From f7af15baf0f739164fbbb761bec52028e29d73f4 Mon Sep 17 00:00:00 2001 From: maqi Date: Tue, 17 Mar 2020 11:02:40 +0800 Subject: [PATCH 001/103] redme.md --- README.md | 3 +++ .../\351\222\211\351\222\211\347\276\244.jpg" | Bin 0 -> 91707 bytes 2 files changed, 3 insertions(+) create mode 100644 "images/\351\222\211\351\222\211\347\276\244.jpg" diff --git a/README.md b/README.md index db25164b6..717343edf 100644 --- a/README.md +++ b/README.md @@ -326,3 +326,6 @@ into # 招聘 1.大数据平台开发工程师,想了解岗位详细信息可以添加本人微信号ysqwhiletrue,注明招聘,如有意者发送简历至sishu@dtstack.com。 +# FlinkX & FlinkStreamSQL钉钉交流群 + +![image](images/钉钉群.jpg) diff --git "a/images/\351\222\211\351\222\211\347\276\244.jpg" "b/images/\351\222\211\351\222\211\347\276\244.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..a005c552544342162169e85ddc053d9f80915240 GIT binary patch literal 91707 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

o*CzL5g5v@2^EcMEV9s<;KPZ~eLjD4%2Jew*Wk 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

qVsND4ALb<)Gug%Ade`XySmg7 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 Date: Tue, 17 Mar 2020 11:09:07 +0800 Subject: [PATCH 002/103] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 717343edf..c32a83f6f 100644 --- a/README.md +++ b/README.md @@ -328,4 +328,6 @@ into # FlinkX & FlinkStreamSQL钉钉交流群 -![image](images/钉钉群.jpg) +

+ +
From 5be698479acdf67e81c786e002009fefff848009 Mon Sep 17 00:00:00 2001 From: chaozwn <326747337@qq.com> Date: Tue, 17 Mar 2020 16:01:45 +0800 Subject: [PATCH 003/103] =?UTF-8?q?=E8=A7=A3=E5=86=B3cep=E4=B8=8D=E8=83=BD?= =?UTF-8?q?=E6=89=8B=E5=8A=A8=E6=8C=87=E5=AE=9Awatermark=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flink/sql/exec/ExecuteProcessHelper.java | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java b/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java index 058ef6097..442ad8ceb 100644 --- a/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java +++ b/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java @@ -80,9 +80,10 @@ import java.util.Set; /** - * 任务执行时的流程方法 + * 任务执行时的流程方法 * Date: 2020/2/17 * Company: www.dtstack.com + * * @author maqi */ public class ExecuteProcessHelper { @@ -131,7 +132,8 @@ public static ParamsInfo parseParams(String[] args) throws Exception { } /** - * 非local模式或者shipfile部署模式,remoteSqlPluginPath必填 + * 非local模式或者shipfile部署模式,remoteSqlPluginPath必填 + * * @param remoteSqlPluginPath * @param deployMode * @param pluginLoadMode @@ -187,14 +189,14 @@ public static List getExternalJarUrls(String addJarListStr) throws java.io. } return jarUrlList; } - + private static void sqlTranslation(String localSqlPluginPath, - StreamTableEnvironment tableEnv, - SqlTree sqlTree,Map sideTableMap, - Map registerTableCache, - StreamQueryConfig queryConfig) throws Exception { + StreamTableEnvironment tableEnv, + SqlTree sqlTree, Map sideTableMap, + Map registerTableCache, + StreamQueryConfig queryConfig) throws Exception { - SideSqlExec sideSqlExec = new SideSqlExec(); + SideSqlExec sideSqlExec = new SideSqlExec(); sideSqlExec.setLocalSqlPluginPath(localSqlPluginPath); for (CreateTmpTableParser.SqlParserResult result : sqlTree.getTmpSqlList()) { sideSqlExec.exec(result.getExecSql(), sideTableMap, tableEnv, registerTableCache, queryConfig, result); @@ -254,13 +256,14 @@ public static void registerUserDefinedFunction(SqlTree sqlTree, List jarUrl } /** - * 向Flink注册源表和结果表,返回执行时插件包的全路径 + * 向Flink注册源表和结果表,返回执行时插件包的全路径 + * * @param sqlTree * @param env * @param tableEnv * @param localSqlPluginPath * @param remoteSqlPluginPath - * @param pluginLoadMode 插件加载模式 classpath or shipfile + * @param pluginLoadMode 插件加载模式 classpath or shipfile * @param sideTableMap * @param registerTableCache * @return @@ -293,7 +296,23 @@ public static Set registerTable(SqlTree sqlTree, StreamExecutionEnvironment if (waterMarkerAssigner.checkNeedAssignWaterMarker(sourceTableInfo)) { adaptStream = waterMarkerAssigner.assignWaterMarker(adaptStream, typeInfo, sourceTableInfo); - fields += ",ROWTIME.ROWTIME"; + String eventTimeField = sourceTableInfo.getEventTimeField(); + boolean hasEventTimeField = false; + if (!Strings.isNullOrEmpty(eventTimeField)) { + String[] fieldArray = fields.split(","); + for (int i = 0; i < fieldArray.length; i++) { + if (fieldArray[i].equals(eventTimeField)) { + fieldArray[i] = eventTimeField + ".ROWTIME"; + hasEventTimeField = true; + break; + } + } + if (hasEventTimeField) { + fields = String.join(",", fieldArray); + } else { + fields += ",ROWTIME.ROWTIME"; + } + } } else { fields += ",PROCTIME.PROCTIME"; } @@ -329,7 +348,8 @@ public static Set registerTable(SqlTree sqlTree, StreamExecutionEnvironment } /** - * perjob模式将job依赖的插件包路径存储到cacheFile,在外围将插件包路径传递给jobgraph + * perjob模式将job依赖的插件包路径存储到cacheFile,在外围将插件包路径传递给jobgraph + * * @param env * @param classPathSet */ From 674d2d94a6e9cb5acdc014d6c7b5ad4fcaacef79 Mon Sep 17 00:00:00 2001 From: todd5167 <313328862@qq.com> Date: Tue, 17 Mar 2020 20:53:56 +0800 Subject: [PATCH 004/103] Update README.md --- README.md | 34 ++++++---------------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index c32a83f6f..491888c90 100644 --- a/README.md +++ b/README.md @@ -6,34 +6,17 @@ > > * 实现了流与维表的join > > * 支持原生FLinkSQL所有的语法 > > * 扩展了输入和输出的性能指标到promethus - - ## 新特性: - * 1.kafka源表支持not null语法,支持字符串类型的时间转换。 - * 2.rdb维表与DB建立连接时,周期进行连接,防止连接断开。rdbsink写入时,对连接进行检查。 - * 3.异步维表支持非等值连接,比如:<>,<,>。 - * 4.增加kafka数组解析 - * 5.增加kafka1.0以上版本的支持 - * 6.增加postgresql、kudu、clickhouse维表、结果表的支持 - * 7.支持插件的依赖方式,参考pluginLoadMode参数 - * 8.支持cep处理 - * 9.支持udaf - * 10.支持谓词下移 - * 11.支持状态的ttl - - ## BUG修复: - * 1.修复不能解析sql中orderby,union语法。 - * 2.修复yarnPer模式提交失败的异常。 - * 3.一些bug的修复 # 已支持 * 源表:kafka 0.9、0.10、0.11、1.x版本 * 维表:mysql, SQlServer,oracle, hbase, mongo, redis, cassandra, serversocket, kudu, postgresql, clickhouse, impala, db2, sqlserver * 结果表:mysql, SQlServer, oracle, hbase, elasticsearch5.x, mongo, redis, cassandra, console, kudu, postgresql, clickhouse, impala, db2, sqlserver - -# 后续开发计划 - * 维表快照 - * kafka avro格式 - * topN + +# FlinkX & FlinkStreamSQL钉钉交流群 + +
+ +
## 1 快速起步 ### 1.1 运行模式 @@ -326,8 +309,3 @@ into # 招聘 1.大数据平台开发工程师,想了解岗位详细信息可以添加本人微信号ysqwhiletrue,注明招聘,如有意者发送简历至sishu@dtstack.com。 -# FlinkX & FlinkStreamSQL钉钉交流群 - -
- -
From 8ee1f7a274a539f90e6e5d7f99e72171103b22af Mon Sep 17 00:00:00 2001 From: whiletrue <670694243@qq.com> Date: Mon, 23 Mar 2020 10:25:05 +0800 Subject: [PATCH 005/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 491888c90..e11a05212 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ * 维表:mysql, SQlServer,oracle, hbase, mongo, redis, cassandra, serversocket, kudu, postgresql, clickhouse, impala, db2, sqlserver * 结果表:mysql, SQlServer, oracle, hbase, elasticsearch5.x, mongo, redis, cassandra, console, kudu, postgresql, clickhouse, impala, db2, sqlserver -# FlinkX & FlinkStreamSQL钉钉交流群 +# 钉钉交流群
From 059454fc0abbaee9292545a6ca9cf90361e1aedb Mon Sep 17 00:00:00 2001 From: whiletrue <670694243@qq.com> Date: Mon, 23 Mar 2020 10:49:00 +0800 Subject: [PATCH 006/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e11a05212..109cfb9b0 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ * 维表:mysql, SQlServer,oracle, hbase, mongo, redis, cassandra, serversocket, kudu, postgresql, clickhouse, impala, db2, sqlserver * 结果表:mysql, SQlServer, oracle, hbase, elasticsearch5.x, mongo, redis, cassandra, console, kudu, postgresql, clickhouse, impala, db2, sqlserver -# 钉钉交流群 +# 钉钉交流群(可以搜索群号30537511,也可以扫下面的二维码)
From 837dd1f0708bac5513a276946b442bbb0b817d57 Mon Sep 17 00:00:00 2001 From: whiletrue <670694243@qq.com> Date: Mon, 23 Mar 2020 11:03:35 +0800 Subject: [PATCH 007/103] Update README.md --- README.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 109cfb9b0..1f2306336 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,14 @@ -# flinkStreamSQL + +## 技术交流 + > * 招聘大数据平台开发工程师,想了解岗位详细信息可以添加本人微信号ysqwhiletrue,注明招聘,如有意者发送简历至sishu@dtstack.com + + > * 可以搜索群号30537511或者可以扫下面的二维码进入钉钉群 +
+ +
+ + +## flinkStreamSQL > * 基于开源的flink,对其实时sql进行扩展 > > * 自定义create table 语法(包括源表,输出表,维表) > > * 自定义create view 语法 @@ -7,16 +17,11 @@ > > * 支持原生FLinkSQL所有的语法 > > * 扩展了输入和输出的性能指标到promethus -# 已支持 +## 已支持 * 源表:kafka 0.9、0.10、0.11、1.x版本 * 维表:mysql, SQlServer,oracle, hbase, mongo, redis, cassandra, serversocket, kudu, postgresql, clickhouse, impala, db2, sqlserver * 结果表:mysql, SQlServer, oracle, hbase, elasticsearch5.x, mongo, redis, cassandra, console, kudu, postgresql, clickhouse, impala, db2, sqlserver - -# 钉钉交流群(可以搜索群号30537511,也可以扫下面的二维码) - -
- -
+ ## 1 快速起步 ### 1.1 运行模式 @@ -306,6 +311,5 @@ into and a.pv=10 ) as d ``` -# 招聘 -1.大数据平台开发工程师,想了解岗位详细信息可以添加本人微信号ysqwhiletrue,注明招聘,如有意者发送简历至sishu@dtstack.com。 + From 3cb8db43a06a41ac20431079cba311ca193d5c12 Mon Sep 17 00:00:00 2001 From: whiletrue <670694243@qq.com> Date: Mon, 23 Mar 2020 11:15:56 +0800 Subject: [PATCH 008/103] Add files via upload --- images/IMG_1573.JPG | Bin 0 -> 199826 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 images/IMG_1573.JPG diff --git a/images/IMG_1573.JPG b/images/IMG_1573.JPG new file mode 100644 index 0000000000000000000000000000000000000000..4a32461c10c6a5256af92e7655010d9d8a03077e GIT binary patch literal 199826 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 z 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%0AgqW|6&KI(E-}bEn-0(AqIvQs+ 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 HbN0Uh?g< Date: Mon, 23 Mar 2020 11:16:34 +0800 Subject: [PATCH 009/103] =?UTF-8?q?Delete=20=E9=92=89=E9=92=89=E7=BE=A4.jp?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\351\222\211\351\222\211\347\276\244.jpg" | Bin 91707 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 "images/\351\222\211\351\222\211\347\276\244.jpg" diff --git "a/images/\351\222\211\351\222\211\347\276\244.jpg" "b/images/\351\222\211\351\222\211\347\276\244.jpg" deleted file mode 100644 index a005c552544342162169e85ddc053d9f80915240..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 91707 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

o*CzL5g5v@2^EcMEV9s<;KPZ~eLjD4%2Jew*Wk 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

qVsND4ALb<)Gug%Ade`XySmg7 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 Date: Mon, 23 Mar 2020 11:18:15 +0800 Subject: [PATCH 010/103] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1f2306336..7a6c936a0 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ ## 技术交流 - > * 招聘大数据平台开发工程师,想了解岗位详细信息可以添加本人微信号ysqwhiletrue,注明招聘,如有意者发送简历至sishu@dtstack.com + * 招聘大数据平台开发工程师,想了解岗位详细信息可以添加本人微信号ysqwhiletrue,注明招聘,如有意者发送简历至sishu@dtstack.com - > * 可以搜索群号30537511或者可以扫下面的二维码进入钉钉群 + * 可以搜索群号30537511或者可以扫下面的二维码进入钉钉群

- +
From a0fb9b3729a80d74dd74d100e2f85c1cf0756316 Mon Sep 17 00:00:00 2001 From: huangxiaowei Date: Tue, 24 Mar 2020 07:21:08 +0800 Subject: [PATCH 011/103] 1. one wrong spelling 2. change some public method to private. --- .../flink/sql/exec/ExecuteProcessHelper.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java b/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java index 058ef6097..571d504cc 100644 --- a/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java +++ b/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java @@ -137,7 +137,7 @@ public static ParamsInfo parseParams(String[] args) throws Exception { * @param pluginLoadMode * @return */ - public static boolean checkRemoteSqlPluginPath(String remoteSqlPluginPath, String deployMode, String pluginLoadMode) { + private static boolean checkRemoteSqlPluginPath(String remoteSqlPluginPath, String deployMode, String pluginLoadMode) { if (StringUtils.isEmpty(remoteSqlPluginPath)) { return StringUtils.equalsIgnoreCase(pluginLoadMode, EPluginLoadMode.SHIPFILE.name()) || StringUtils.equalsIgnoreCase(deployMode, ClusterMode.local.name()); @@ -174,7 +174,7 @@ public static StreamExecutionEnvironment getStreamExecution(ParamsInfo paramsInf } - public static List getExternalJarUrls(String addJarListStr) throws java.io.IOException { + private static List getExternalJarUrls(String addJarListStr) throws java.io.IOException { List jarUrlList = Lists.newArrayList(); if (Strings.isNullOrEmpty(addJarListStr)) { return jarUrlList; @@ -187,7 +187,7 @@ public static List getExternalJarUrls(String addJarListStr) throws java.io. } return jarUrlList; } - + private static void sqlTranslation(String localSqlPluginPath, StreamTableEnvironment tableEnv, SqlTree sqlTree,Map sideTableMap, @@ -238,7 +238,7 @@ private static void sqlTranslation(String localSqlPluginPath, } } - public static void registerUserDefinedFunction(SqlTree sqlTree, List jarUrlList, TableEnvironment tableEnv) + private static void registerUserDefinedFunction(SqlTree sqlTree, List jarUrlList, TableEnvironment tableEnv) throws IllegalAccessException, InvocationTargetException { // udf和tableEnv须由同一个类加载器加载 ClassLoader levelClassLoader = tableEnv.getClass().getClassLoader(); @@ -266,9 +266,9 @@ public static void registerUserDefinedFunction(SqlTree sqlTree, List jarUrl * @return * @throws Exception */ - public static Set registerTable(SqlTree sqlTree, StreamExecutionEnvironment env, StreamTableEnvironment tableEnv, String localSqlPluginPath, + private static Set registerTable(SqlTree sqlTree, StreamExecutionEnvironment env, StreamTableEnvironment tableEnv, String localSqlPluginPath, String remoteSqlPluginPath, String pluginLoadMode, Map sideTableMap, Map registerTableCache) throws Exception { - Set pluginClassPatshSets = Sets.newHashSet(); + Set pluginClassPathSets = Sets.newHashSet(); WaterMarkerAssigner waterMarkerAssigner = new WaterMarkerAssigner(); for (TableInfo tableInfo : sqlTree.getTableInfoMap().values()) { @@ -306,7 +306,7 @@ public static Set registerTable(SqlTree sqlTree, StreamExecutionEnvironment registerTableCache.put(tableInfo.getName(), regTable); URL sourceTablePathUrl = PluginUtil.buildSourceAndSinkPathByLoadMode(tableInfo.getType(), SourceTableInfo.SOURCE_SUFFIX, localSqlPluginPath, remoteSqlPluginPath, pluginLoadMode); - pluginClassPatshSets.add(sourceTablePathUrl); + pluginClassPathSets.add(sourceTablePathUrl); } else if (tableInfo instanceof TargetTableInfo) { TableSink tableSink = StreamSinkFactory.getTableSink((TargetTableInfo) tableInfo, localSqlPluginPath); @@ -314,18 +314,18 @@ public static Set registerTable(SqlTree sqlTree, StreamExecutionEnvironment tableEnv.registerTableSink(tableInfo.getName(), tableInfo.getFields(), flinkTypes, tableSink); URL sinkTablePathUrl = PluginUtil.buildSourceAndSinkPathByLoadMode(tableInfo.getType(), TargetTableInfo.TARGET_SUFFIX, localSqlPluginPath, remoteSqlPluginPath, pluginLoadMode); - pluginClassPatshSets.add(sinkTablePathUrl); + pluginClassPathSets.add(sinkTablePathUrl); } else if (tableInfo instanceof SideTableInfo) { String sideOperator = ECacheType.ALL.name().equals(((SideTableInfo) tableInfo).getCacheType()) ? "all" : "async"; sideTableMap.put(tableInfo.getName(), (SideTableInfo) tableInfo); URL sideTablePathUrl = PluginUtil.buildSidePathByLoadMode(tableInfo.getType(), sideOperator, SideTableInfo.TARGET_SUFFIX, localSqlPluginPath, remoteSqlPluginPath, pluginLoadMode); - pluginClassPatshSets.add(sideTablePathUrl); + pluginClassPathSets.add(sideTablePathUrl); } else { throw new RuntimeException("not support table type:" + tableInfo.getType()); } } - return pluginClassPatshSets; + return pluginClassPathSets; } /** @@ -333,7 +333,7 @@ public static Set registerTable(SqlTree sqlTree, StreamExecutionEnvironment * @param env * @param classPathSet */ - public static void registerPluginUrlToCachedFile(StreamExecutionEnvironment env, Set classPathSet) { + private static void registerPluginUrlToCachedFile(StreamExecutionEnvironment env, Set classPathSet) { int i = 0; for (URL url : classPathSet) { String classFileName = String.format(CLASS_FILE_NAME_FMT, i); @@ -342,7 +342,7 @@ public static void registerPluginUrlToCachedFile(StreamExecutionEnvironment env, } } - public static StreamExecutionEnvironment getStreamExeEnv(Properties confProperties, String deployMode) throws Exception { + private static StreamExecutionEnvironment getStreamExeEnv(Properties confProperties, String deployMode) throws Exception { StreamExecutionEnvironment env = !ClusterMode.local.name().equals(deployMode) ? StreamExecutionEnvironment.getExecutionEnvironment() : new MyLocalStreamEnvironment(); From f919e35185846ee4330882d0f97e3375f2055503 Mon Sep 17 00:00:00 2001 From: tiezhu Date: Thu, 9 Apr 2020 11:27:10 +0800 Subject: [PATCH 012/103] =?UTF-8?q?add=20isHighAvailability=20=E5=88=A4?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sql/launcher/ClusterClientFactory.java | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/launcher/src/main/java/com/dtstack/flink/sql/launcher/ClusterClientFactory.java b/launcher/src/main/java/com/dtstack/flink/sql/launcher/ClusterClientFactory.java index 14cd847b5..eb06b39ba 100644 --- a/launcher/src/main/java/com/dtstack/flink/sql/launcher/ClusterClientFactory.java +++ b/launcher/src/main/java/com/dtstack/flink/sql/launcher/ClusterClientFactory.java @@ -24,15 +24,12 @@ import org.apache.commons.io.Charsets; import org.apache.commons.lang.StringUtils; import org.apache.flink.client.program.ClusterClient; -import org.apache.flink.client.program.MiniClusterClient; -import org.apache.flink.configuration.ConfigConstants; +import org.apache.flink.client.program.rest.RestClusterClient; import org.apache.flink.configuration.Configuration; import org.apache.flink.configuration.GlobalConfiguration; import org.apache.flink.configuration.JobManagerOptions; import org.apache.flink.core.fs.FileSystem; import org.apache.flink.runtime.akka.AkkaUtils; -import org.apache.flink.runtime.minicluster.MiniCluster; -import org.apache.flink.runtime.minicluster.MiniClusterConfiguration; import org.apache.flink.runtime.util.LeaderConnectionInfo; import org.apache.flink.yarn.AbstractYarnClusterDescriptor; import org.apache.flink.yarn.YarnClusterDescriptor; @@ -57,6 +54,18 @@ */ public class ClusterClientFactory { + private static final Logger LOG = LoggerFactory.getLogger(ClusterClientFactory.class); + + private static final String HA_CLUSTER_ID = "high-availability.cluster-id"; + + private static final String HIGH_AVAILABILITY = "high-availability"; + + private static final String NODE = "NONE"; + + private static final String ZOOKEEPER = "zookeeper"; + + private static final String HADOOP_CONF = "fs.hdfs.hadoopconf"; + public static ClusterClient createClusterClient(Options launcherOptions) throws Exception { String mode = launcherOptions.getMode(); if (mode.equals(ClusterMode.standalone.name())) { @@ -70,10 +79,12 @@ public static ClusterClient createClusterClient(Options launcherOptions) throws public static ClusterClient createStandaloneClient(Options launcherOptions) throws Exception { String flinkConfDir = launcherOptions.getFlinkconf(); Configuration config = GlobalConfiguration.loadConfiguration(flinkConfDir); - MiniClusterConfiguration.Builder configBuilder = new MiniClusterConfiguration.Builder(); - configBuilder.setConfiguration(config); - MiniCluster miniCluster = new MiniCluster(configBuilder.build()); - MiniClusterClient clusterClient = new MiniClusterClient(config, miniCluster); + + LOG.info("------------config params-------------------------"); + config.toMap().forEach((key, value) -> LOG.info("{}: {}", key, value)); + LOG.info("-------------------------------------------"); + + RestClusterClient clusterClient = new RestClusterClient<>(config, "clusterClient"); LeaderConnectionInfo connectionInfo = clusterClient.getClusterConnectionInfo(); InetSocketAddress address = AkkaUtils.getInetSocketAddressFromAkkaURL(connectionInfo.getAddress()); config.setString(JobManagerOptions.ADDRESS, address.getAddress().getHostName()); @@ -89,6 +100,8 @@ public static ClusterClient createYarnSessionClient(Options launcherOptions) { if (StringUtils.isNotBlank(yarnConfDir)) { try { + boolean isHighAvailability; + config.setString(ConfigConstants.PATH_HADOOP_CONFIG, yarnConfDir); FileSystem.initialize(config); @@ -101,6 +114,7 @@ public static ClusterClient createYarnSessionClient(Options launcherOptions) { String yarnSessionConf = launcherOptions.getYarnSessionConf(); yarnSessionConf = URLDecoder.decode(yarnSessionConf, Charsets.UTF_8.toString()); Properties yarnSessionConfProperties = PluginUtil.jsonStrToObject(yarnSessionConf, Properties.class); + Object yid = yarnSessionConfProperties.get("yid"); if (null != yid) { @@ -109,12 +123,22 @@ public static ClusterClient createYarnSessionClient(Options launcherOptions) { applicationId = getYarnClusterApplicationId(yarnClient); } - System.out.println("applicationId=" + applicationId.toString()); + LOG.info("current applicationId = {}", applicationId.toString()); if (StringUtils.isEmpty(applicationId.toString())) { throw new RuntimeException("No flink session found on yarn cluster."); } + isHighAvailability = config.getString(HIGH_AVAILABILITY, NODE).equals(ZOOKEEPER); + + if (isHighAvailability && config.getString(HA_CLUSTER_ID, null) == null) { + config.setString(HA_CLUSTER_ID, applicationId.toString()); + } + + LOG.info("------------config params-------------------------"); + config.toMap().forEach((key, value) -> LOG.info("{}: {}", key, value)); + LOG.info("-------------------------------------------"); + AbstractYarnClusterDescriptor clusterDescriptor = new YarnClusterDescriptor(config, yarnConf, flinkConfDir, yarnClient, false); ClusterClient clusterClient = clusterDescriptor.retrieve(applicationId); clusterClient.setDetached(true); @@ -158,7 +182,7 @@ private static ApplicationId getYarnClusterApplicationId(YarnClient yarnClient) } - if (StringUtils.isEmpty(applicationId.toString())) { + if (applicationId == null || StringUtils.isEmpty(applicationId.toString())) { throw new RuntimeException("No flink session found on yarn cluster."); } return applicationId; @@ -166,7 +190,7 @@ private static ApplicationId getYarnClusterApplicationId(YarnClient yarnClient) private static ApplicationId toApplicationId(String appIdStr) { Iterator it = StringHelper._split(appIdStr).iterator(); - if (!(it.next()).equals("application")) { + if (!"application".equals(it.next())) { throw new IllegalArgumentException("Invalid ApplicationId prefix: " + appIdStr + ". The valid ApplicationId should start with prefix " + "application"); } else { try { From e438f544bf0a82de74ad665abca61b95c8fddfcf Mon Sep 17 00:00:00 2001 From: tiezhu Date: Fri, 10 Apr 2020 09:34:55 +0800 Subject: [PATCH 013/103] fix --- .../flink/sql/launcher/LauncherMain.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/launcher/src/main/java/com/dtstack/flink/sql/launcher/LauncherMain.java b/launcher/src/main/java/com/dtstack/flink/sql/launcher/LauncherMain.java index c098bc27e..cb1fffe01 100644 --- a/launcher/src/main/java/com/dtstack/flink/sql/launcher/LauncherMain.java +++ b/launcher/src/main/java/com/dtstack/flink/sql/launcher/LauncherMain.java @@ -16,7 +16,7 @@ * limitations under the License. */ - + package com.dtstack.flink.sql.launcher; @@ -66,8 +66,7 @@ public class LauncherMain { private static String getLocalCoreJarPath(String localSqlRootJar) throws Exception { String jarPath = PluginUtil.getCoreJarFileName(localSqlRootJar, CORE_JAR); - String corePath = localSqlRootJar + SP + jarPath; - return corePath; + return localSqlRootJar + SP + jarPath; } public static void main(String[] args) throws Exception { @@ -104,6 +103,7 @@ public static void main(String[] args) throws Exception { String flinkConfDir = launcherOptions.getFlinkconf(); Configuration config = StringUtils.isEmpty(flinkConfDir) ? new Configuration() : GlobalConfiguration.loadConfiguration(flinkConfDir); JobGraph jobGraph = PackagedProgramUtils.createJobGraph(program, config, 1); + PerJobSubmitter.submit(launcherOptions, jobGraph, config); } else { ClusterClient clusterClient = ClusterClientFactory.createClusterClient(launcherOptions); @@ -115,20 +115,20 @@ public static void main(String[] args) throws Exception { private static String[] parseJson(String[] args) { BufferedReader reader = null; - String lastStr = ""; - try{ + StringBuilder lastStr = new StringBuilder(); + try { FileInputStream fileInputStream = new FileInputStream(args[0]); - InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "UTF-8"); + InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, Charsets.UTF_8); reader = new BufferedReader(inputStreamReader); - String tempString = null; - while((tempString = reader.readLine()) != null){ - lastStr += tempString; + String tempString; + while ((tempString = reader.readLine()) != null) { + lastStr.append(tempString); } reader.close(); - }catch(IOException e){ + } catch (IOException e) { e.printStackTrace(); - }finally{ - if(reader != null){ + } finally { + if (reader != null) { try { reader.close(); } catch (IOException e) { @@ -136,14 +136,14 @@ private static String[] parseJson(String[] args) { } } } - Map map = JSON.parseObject(lastStr, new TypeReference>(){} ); + Map map = JSON.parseObject(lastStr.toString(), new TypeReference>() { + }); List list = new LinkedList<>(); for (Map.Entry entry : map.entrySet()) { list.add("-" + entry.getKey()); list.add(entry.getValue().toString()); } - String[] array = list.toArray(new String[list.size()]); - return array; + return list.toArray(new String[0]); } } From 83429b26199feac0f30a5ec15c2e230c670a4b78 Mon Sep 17 00:00:00 2001 From: xuchao Date: Mon, 20 Apr 2020 19:50:17 +0800 Subject: [PATCH 014/103] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E8=BF=87=E7=A8=8B=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/dtstack/flink/sql/launcher/ClusterClientFactory.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/launcher/src/main/java/com/dtstack/flink/sql/launcher/ClusterClientFactory.java b/launcher/src/main/java/com/dtstack/flink/sql/launcher/ClusterClientFactory.java index eb06b39ba..cfbab1023 100644 --- a/launcher/src/main/java/com/dtstack/flink/sql/launcher/ClusterClientFactory.java +++ b/launcher/src/main/java/com/dtstack/flink/sql/launcher/ClusterClientFactory.java @@ -25,6 +25,7 @@ import org.apache.commons.lang.StringUtils; import org.apache.flink.client.program.ClusterClient; import org.apache.flink.client.program.rest.RestClusterClient; +import org.apache.flink.configuration.ConfigConstants; import org.apache.flink.configuration.Configuration; import org.apache.flink.configuration.GlobalConfiguration; import org.apache.flink.configuration.JobManagerOptions; @@ -39,6 +40,8 @@ import org.apache.hadoop.yarn.client.api.YarnClient; import org.apache.hadoop.yarn.conf.YarnConfiguration; import org.apache.hadoop.yarn.util.StringHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.net.InetSocketAddress; import java.net.URLDecoder; From 6f8c9aa6c560c47b42bfd51684481e1054abfc20 Mon Sep 17 00:00:00 2001 From: tiezhu Date: Tue, 21 Apr 2020 19:53:54 +0800 Subject: [PATCH 015/103] =?UTF-8?q?fix=20=E5=88=AB=E5=90=8Djoin=E5=BC=82?= =?UTF-8?q?=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/dtstack/flink/sql/side/SideInfo.java | 2 +- .../flink/sql/side/rdb/async/RdbAsyncSideInfo.java | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/side/SideInfo.java b/core/src/main/java/com/dtstack/flink/sql/side/SideInfo.java index 029c86e25..4b53f77f7 100644 --- a/core/src/main/java/com/dtstack/flink/sql/side/SideInfo.java +++ b/core/src/main/java/com/dtstack/flink/sql/side/SideInfo.java @@ -93,7 +93,7 @@ public void parseSelectFields(JoinInfo joinInfo){ if(fieldInfo.getTable().equalsIgnoreCase(sideTableName)){ String sideFieldName = sideTableInfo.getPhysicalFields().getOrDefault(fieldInfo.getFieldName(), fieldInfo.getFieldName()); fields.add(sideFieldName); - sideSelectFieldsType.put(sideTableFieldIndex, getTargetFieldType(sideFieldName)); + sideSelectFieldsType.put(sideTableFieldIndex, getTargetFieldType(fieldInfo.getFieldName())); sideFieldIndex.put(i, sideTableFieldIndex); sideFieldNameIndex.put(i, sideFieldName); sideTableFieldIndex++; diff --git a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncSideInfo.java b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncSideInfo.java index 9e8c13080..f87b27ae4 100644 --- a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncSideInfo.java +++ b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncSideInfo.java @@ -18,8 +18,6 @@ package com.dtstack.flink.sql.side.rdb.async; -import org.apache.flink.api.java.typeutils.RowTypeInfo; - import com.dtstack.flink.sql.side.FieldInfo; import com.dtstack.flink.sql.side.JoinInfo; import com.dtstack.flink.sql.side.PredicateInfo; @@ -33,9 +31,11 @@ import org.apache.calcite.sql.SqlKind; import org.apache.calcite.sql.SqlNode; import org.apache.commons.lang3.StringUtils; +import org.apache.flink.api.java.typeutils.RowTypeInfo; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @@ -86,6 +86,7 @@ public void dealOneEqualCon(SqlNode sqlNode, String sideTableName) { SqlIdentifier left = (SqlIdentifier) ((SqlBasicCall) sqlNode).getOperands()[0]; SqlIdentifier right = (SqlIdentifier) ((SqlBasicCall) sqlNode).getOperands()[1]; + Map physicalFields = sideTableInfo.getPhysicalFields(); String leftTableName = left.getComponent(0).getSimple(); String leftField = left.getComponent(1).getSimple(); @@ -94,7 +95,7 @@ public void dealOneEqualCon(SqlNode sqlNode, String sideTableName) { String rightField = right.getComponent(1).getSimple(); if (leftTableName.equalsIgnoreCase(sideTableName)) { - equalFieldList.add(leftField); + equalFieldList.add(physicalFields.get(leftField)); int equalFieldIndex = -1; for (int i = 0; i < rowTypeInfo.getFieldNames().length; i++) { String fieldName = rowTypeInfo.getFieldNames()[i]; @@ -110,7 +111,7 @@ public void dealOneEqualCon(SqlNode sqlNode, String sideTableName) { } else if (rightTableName.equalsIgnoreCase(sideTableName)) { - equalFieldList.add(rightField); + equalFieldList.add(physicalFields.get(rightField)); int equalFieldIndex = -1; for (int i = 0; i < rowTypeInfo.getFieldNames().length; i++) { String fieldName = rowTypeInfo.getFieldNames()[i]; From a9f9299d9b1d9624e921c0f8cce7f296d2b9ea60 Mon Sep 17 00:00:00 2001 From: whiletrue Date: Tue, 26 May 2020 20:04:34 +0800 Subject: [PATCH 016/103] Create SECURITY.md --- SECURITY.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..034e84803 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. From 49e5a257475096bccb72107131338fa6191d8c5d Mon Sep 17 00:00:00 2001 From: dapeng Date: Wed, 26 Aug 2020 11:35:56 +0800 Subject: [PATCH 017/103] =?UTF-8?q?fix=20=E5=86=85=E5=AD=98=E6=BA=A2?= =?UTF-8?q?=E5=87=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elasticsearch6/Elasticsearch6AsyncReqRow.java | 11 +++++++++-- .../flink/sql/side/elasticsearch6/util/Es6Util.java | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/elasticsearch6/elasticsearch6-side/elasticsearch6-async-side/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/Elasticsearch6AsyncReqRow.java b/elasticsearch6/elasticsearch6-side/elasticsearch6-async-side/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/Elasticsearch6AsyncReqRow.java index fa6d507bb..8be1c1f3a 100644 --- a/elasticsearch6/elasticsearch6-side/elasticsearch6-async-side/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/Elasticsearch6AsyncReqRow.java +++ b/elasticsearch6/elasticsearch6-side/elasticsearch6-async-side/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/Elasticsearch6AsyncReqRow.java @@ -70,6 +70,7 @@ public class Elasticsearch6AsyncReqRow extends BaseAsyncReqRow implements Serial private transient RestHighLevelClient rhlClient; private SearchRequest searchRequest; private List sqlJoinCompareOperate = Lists.newArrayList(); + private static final Integer MAX_ROW_NUM = 50000; public Elasticsearch6AsyncReqRow(RowTypeInfo rowTypeInfo, JoinInfo joinInfo, List outFieldInfoList, AbstractSideTableInfo sideTableInfo) { super(new Elasticsearch6AsyncSideInfo(rowTypeInfo, joinInfo, outFieldInfoList, sideTableInfo)); @@ -92,7 +93,7 @@ public void handleAsyncInvoke(Map inputParams, CRow input, Resul String key = buildCacheKey(inputParams); BoolQueryBuilder boolQueryBuilder = Es6Util.setPredicateclause(sideInfo); boolQueryBuilder = setInputParams(inputParams, boolQueryBuilder); - SearchSourceBuilder searchSourceBuilder = initConfiguration(); + SearchSourceBuilder searchSourceBuilder = initConfiguration(inputParams); searchSourceBuilder.query(boolQueryBuilder); searchRequest.source(searchSourceBuilder); @@ -116,6 +117,11 @@ public void onResponse(SearchResponse searchResponse) { if (searchHits.length < getFetchSize()) { break; } + //protect memory + if (rowList.size() >= MAX_ROW_NUM) { + LOG.warn("row size beyond limit"); + break; + } if (tableInfo == null && tmpRhlClient == null) { // create new connection to fetch data tableInfo = (Elasticsearch6SideTableInfo) sideInfo.getSideTableInfo(); @@ -225,10 +231,11 @@ public void close() throws Exception { } - private SearchSourceBuilder initConfiguration() { + private SearchSourceBuilder initConfiguration(Map inputParams) { SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.size(getFetchSize()); searchSourceBuilder.sort("_id", SortOrder.DESC); + inputParams.keySet().stream().forEach(k -> searchSourceBuilder.sort(k, SortOrder.DESC)); String[] sideFieldNames = StringUtils.split(sideInfo.getSideSelectFields().trim(), ","); searchSourceBuilder.fetchSource(sideFieldNames, null); diff --git a/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/Es6Util.java b/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/Es6Util.java index bc5637515..4a25b43d9 100644 --- a/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/Es6Util.java +++ b/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/Es6Util.java @@ -171,6 +171,9 @@ public static BoolQueryBuilder buildFilterCondition(BoolQueryBuilder boolQueryBu return boolQueryBuilder.must(QueryBuilders.existsQuery(info.getFieldName())); case "=": case "EQUALS": + if(StringUtils.isBlank(info.getCondition())){ + return boolQueryBuilder; + } return boolQueryBuilder.must(QueryBuilders.termQuery(textConvertToKeyword(info.getFieldName(), sideInfo), removeSpaceAndApostrophe(info.getCondition())[0])); case "<>": case "NOT_EQUALS": From 6cc401fefb29eccbcb53c94bdf8cce2be7204daf Mon Sep 17 00:00:00 2001 From: dapeng Date: Tue, 1 Sep 2020 09:56:43 +0800 Subject: [PATCH 018/103] =?UTF-8?q?=E5=8D=87=E7=BA=A7flink=E7=9A=84?= =?UTF-8?q?=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 374ec3c31..0496c6dd8 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ UTF-8 - 1.8.1 + 1.8.3 From 9e7bd2b4b7b0ed898549dd7185cc62e16d9b1e36 Mon Sep 17 00:00:00 2001 From: wuren Date: Tue, 1 Sep 2020 16:30:52 +0800 Subject: [PATCH 019/103] fix array type parse error --- .../flink/sql/table/AbstractTableParser.java | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableParser.java b/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableParser.java index b5c463cb6..ade1ebb42 100644 --- a/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableParser.java +++ b/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableParser.java @@ -24,6 +24,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.apache.commons.lang3.StringUtils; +import org.apache.flink.api.java.tuple.Tuple2; import java.util.List; import java.util.Map; @@ -47,6 +48,8 @@ public abstract class AbstractTableParser { private static Pattern nestJsonFieldKeyPattern = Pattern.compile("(?i)((@*\\S+\\.)*\\S+)\\s+(.+?)\\s+AS\\s+(\\w+)(\\s+NOT\\s+NULL)?$"); private static Pattern physicalFieldFunPattern = Pattern.compile("\\w+\\((\\w+)\\)$"); private static Pattern charTypePattern = Pattern.compile("(?i)CHAR\\((\\d*)\\)$"); + private static Pattern typePattern = Pattern.compile("(\\S+)\\s+(.+)"); + private Map patternMap = Maps.newHashMap(); @@ -93,23 +96,16 @@ public void parseFieldsInfo(String fieldsInfo, AbstractTableInfo tableInfo) { throw new RuntimeException(String.format("table [%s],exists field empty.", tableInfo.getName())); } - String[] fieldInfoArr = fieldRow.split("\\s+"); - - String errorMsg = String.format("table [%s] field [%s] format error.", tableInfo.getName(), fieldRow); - Preconditions.checkState(fieldInfoArr.length >= 2, errorMsg); - boolean isMatcherKey = dealKeyPattern(fieldRow, tableInfo); if (isMatcherKey) { continue; } - //Compatible situation may arise in space in the fieldName - String[] filedNameArr = new String[fieldInfoArr.length - 1]; - System.arraycopy(fieldInfoArr, 0, filedNameArr, 0, fieldInfoArr.length - 1); - String fieldName = String.join(" ", filedNameArr); - String fieldType = fieldInfoArr[fieldInfoArr.length - 1 ].trim(); + Tuple2 t = extractType(fieldRow, tableInfo.getName()); + String fieldName = t.f0; + String fieldType = t.f1; - Class fieldClass = null; + Class fieldClass; AbstractTableInfo.FieldExtraInfo fieldExtraInfo = null; Matcher matcher = charTypePattern.matcher(fieldType); @@ -121,7 +117,7 @@ public void parseFieldsInfo(String fieldsInfo, AbstractTableInfo tableInfo) { fieldClass = dbTypeConvertToJavaType(fieldType); } - tableInfo.addPhysicalMappings(fieldInfoArr[0], fieldInfoArr[0]); + tableInfo.addPhysicalMappings(fieldName, fieldName); tableInfo.addField(fieldName); tableInfo.addFieldClass(fieldClass); tableInfo.addFieldType(fieldType); @@ -131,11 +127,23 @@ public void parseFieldsInfo(String fieldsInfo, AbstractTableInfo tableInfo) { tableInfo.finish(); } + private Tuple2 extractType(String fieldRow, String tableName) { + Matcher matcher = typePattern.matcher(fieldRow); + if (matcher.matches()) { + String fieldName = matcher.group(1); + String fieldType = matcher.group(2); + return Tuple2.of(fieldName, fieldType); + } else { + String errorMsg = String.format("table [%s] field [%s] format error.", tableName, fieldRow); + throw new RuntimeException(errorMsg); + } + } + public void dealPrimaryKey(Matcher matcher, AbstractTableInfo tableInfo) { String primaryFields = matcher.group(1).trim(); - String[] splitArry = primaryFields.split(","); - List primaryKes = Lists.newArrayList(splitArry); - tableInfo.setPrimaryKeys(primaryKes); + String[] splitArray = primaryFields.split(","); + List primaryKeys = Lists.newArrayList(splitArray); + tableInfo.setPrimaryKeys(primaryKeys); } /** From e7e64e9958a7c05e708ec0191e952dc9ecd6c5fb Mon Sep 17 00:00:00 2001 From: wuren Date: Wed, 2 Sep 2020 09:26:14 +0800 Subject: [PATCH 020/103] fix regular of making error msg invalid --- .../java/com/dtstack/flink/sql/table/AbstractTableParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableParser.java b/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableParser.java index ade1ebb42..f0898cfd6 100644 --- a/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableParser.java +++ b/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableParser.java @@ -48,7 +48,7 @@ public abstract class AbstractTableParser { private static Pattern nestJsonFieldKeyPattern = Pattern.compile("(?i)((@*\\S+\\.)*\\S+)\\s+(.+?)\\s+AS\\s+(\\w+)(\\s+NOT\\s+NULL)?$"); private static Pattern physicalFieldFunPattern = Pattern.compile("\\w+\\((\\w+)\\)$"); private static Pattern charTypePattern = Pattern.compile("(?i)CHAR\\((\\d*)\\)$"); - private static Pattern typePattern = Pattern.compile("(\\S+)\\s+(.+)"); + private static Pattern typePattern = Pattern.compile("(\\S+)\\s+(\\w+.*)"); private Map patternMap = Maps.newHashMap(); From 533725bf1739667062d61dde97e101972ccff15b Mon Sep 17 00:00:00 2001 From: chuixue Date: Thu, 3 Sep 2020 16:39:03 +0800 Subject: [PATCH 021/103] hbase sink failed --- .../java/com/dtstack/flink/sql/sink/hbase/RowKeyBuilder.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/RowKeyBuilder.java b/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/RowKeyBuilder.java index 7af51be3d..13329faed 100644 --- a/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/RowKeyBuilder.java +++ b/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/RowKeyBuilder.java @@ -110,10 +110,6 @@ public static String[] splitIgnoreQuotaBrackets(String str, String delimiter){ public ReplaceInfo getReplaceInfo(String field){ field = field.trim(); - if(field.length() <= 2){ - throw new RuntimeException(field + " \n" + - "Format defined exceptions"); - } //判断是不是常量==>''包裹的标识 if(field.startsWith("'") && field.endsWith("'")){ From 3b433faca7bee93e333cf695305b7698016af4a2 Mon Sep 17 00:00:00 2001 From: chuixue Date: Thu, 3 Sep 2020 17:11:11 +0800 Subject: [PATCH 022/103] hbase sink failed doc fixed --- docs/plugin/hbaseSink.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/plugin/hbaseSink.md b/docs/plugin/hbaseSink.md index 5006f11a2..a5ad12067 100644 --- a/docs/plugin/hbaseSink.md +++ b/docs/plugin/hbaseSink.md @@ -7,7 +7,7 @@ CREATE TABLE MyResult( type ='hbase', zookeeperQuorum ='ip:port[,ip:port]', tableName ='tableName', - rowKey ='colName[,colName]', + rowKey ='colName[+colName]', parallelism ='1', zookeeperParent ='/hbase' ) @@ -34,7 +34,7 @@ hbase2.0 |zookeeperQuorum | hbase zk地址,多个直接用逗号隔开|是|| |zookeeperParent | zkParent 路径|是|| |tableName | 关联的hbase表名称|是|| -|rowkey | hbase的rowkey关联的列信息,多个值以逗号隔开|是|| +|rowkey | hbase的rowkey关联的列信息,多个值以'+'连接|是|| |updateMode|APPEND:不回撤数据,只下发增量数据,UPSERT:先删除回撤数据,然后更新|否|APPEND| |parallelism | 并行度设置|否|1| |kerberosAuthEnable | 是否开启kerberos认证|否|false| @@ -76,7 +76,7 @@ CREATE TABLE MyResult( tableName ='myresult', partitionedJoin ='false', parallelism ='1', - rowKey='name,channel' + rowKey='name+channel' ); insert @@ -141,7 +141,7 @@ into ## 6.hbase数据 ### 数据内容说明 -hbase的rowkey 构建规则:以描述的rowkey字段值作为key,多个字段以'-'连接 +hbase的rowkey 构建规则:以描述的rowkey字段值作为key,多个字段以'+'连接 ### 数据内容示例 hbase(main):007:0> scan 'myresult' ROW COLUMN+CELL From 278bcc4e13cea6e207c1ab436a17a258f4d4e2c8 Mon Sep 17 00:00:00 2001 From: dapeng Date: Tue, 8 Sep 2020 15:01:46 +0800 Subject: [PATCH 023/103] =?UTF-8?q?=E4=BF=AE=E5=A4=8Doracle=20=E8=81=94?= =?UTF-8?q?=E5=90=88=E4=B8=BB=E9=94=AE=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/dtstack/flink/sql/sink/oracle/OracleDialect.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oracle/oracle-sink/src/main/java/com/dtstack/flink/sql/sink/oracle/OracleDialect.java b/oracle/oracle-sink/src/main/java/com/dtstack/flink/sql/sink/oracle/OracleDialect.java index 3a320658c..590ecc7a1 100644 --- a/oracle/oracle-sink/src/main/java/com/dtstack/flink/sql/sink/oracle/OracleDialect.java +++ b/oracle/oracle-sink/src/main/java/com/dtstack/flink/sql/sink/oracle/OracleDialect.java @@ -104,7 +104,7 @@ private String buildConnectionByAllReplace(boolean allReplace, String col) { private String buildConnectionConditions(String[] uniqueKeyFields) { - return Arrays.stream(uniqueKeyFields).map(col -> "T1." + quoteIdentifier(col) + "=T2." + quoteIdentifier(col)).collect(Collectors.joining(",")); + return Arrays.stream(uniqueKeyFields).map(col -> "T1." + quoteIdentifier(col) + "=T2." + quoteIdentifier(col)).collect(Collectors.joining(" AND ")); } /** From 840edc74fec5ef94fce45450d27d8121f80962c2 Mon Sep 17 00:00:00 2001 From: dapeng Date: Tue, 8 Sep 2020 16:40:05 +0800 Subject: [PATCH 024/103] =?UTF-8?q?=E8=A1=A8=E4=B8=8D=E5=AD=98=E5=9C=A8?= =?UTF-8?q?=E6=97=B6=E4=BB=BB=E5=8A=A1=E7=BB=93=E6=9D=9F=E9=80=80=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flink/sql/sink/rdb/writer/AppendOnlyWriter.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AppendOnlyWriter.java b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AppendOnlyWriter.java index 7c3ff4b09..9dd06d6a0 100644 --- a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AppendOnlyWriter.java +++ b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AppendOnlyWriter.java @@ -118,11 +118,18 @@ public void executeUpdate(Connection connection) { } catch (SQLException e1) { throw new RuntimeException(e1); } + + if(e.getMessage().contains("doesn't exist")){ + throw new RuntimeException("table not exist"); + + } if (metricOutputFormat.outDirtyRecords.getCount() % DIRTYDATA_PRINT_FREQUENTY == 0 || LOG.isDebugEnabled()) { LOG.error("record insert failed ,this row is {}", row.toString()); LOG.error("", e); } metricOutputFormat.outDirtyRecords.inc(); + + } }); rows.clear(); From 1586a62f0d97695c5af49b6c290375c616b262ff Mon Sep 17 00:00:00 2001 From: dapeng Date: Tue, 8 Sep 2020 17:12:53 +0800 Subject: [PATCH 025/103] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E5=A3=B0=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/dtstack/flink/sql/sink/rdb/writer/AppendOnlyWriter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AppendOnlyWriter.java b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AppendOnlyWriter.java index 9dd06d6a0..b1d601c22 100644 --- a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AppendOnlyWriter.java +++ b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AppendOnlyWriter.java @@ -120,7 +120,7 @@ public void executeUpdate(Connection connection) { } if(e.getMessage().contains("doesn't exist")){ - throw new RuntimeException("table not exist"); + throw new RuntimeException(e); } if (metricOutputFormat.outDirtyRecords.getCount() % DIRTYDATA_PRINT_FREQUENTY == 0 || LOG.isDebugEnabled()) { From df5fcccb398071cf65c7f361c2d326e575e504ee Mon Sep 17 00:00:00 2001 From: dapeng Date: Fri, 18 Sep 2020 09:55:21 +0800 Subject: [PATCH 026/103] =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E4=B8=8D=E4=B8=80=E8=87=B4=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/main/java/com/dtstack/flink/sql/util/ClassUtil.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/main/java/com/dtstack/flink/sql/util/ClassUtil.java b/core/src/main/java/com/dtstack/flink/sql/util/ClassUtil.java index ec88bdbe9..cad1c47cb 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/ClassUtil.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/ClassUtil.java @@ -48,8 +48,10 @@ public static Class stringConvertClass(String str) { return Boolean.class; case "smallint": + return Short.class; case "smallintunsigned": case "tinyint": + return Byte.class; case "tinyintunsigned": case "mediumint": case "mediumintunsigned": From 5d0bd0c840a4976bc8392e8ac000c8d68d823366 Mon Sep 17 00:00:00 2001 From: dapeng Date: Fri, 18 Sep 2020 11:07:59 +0800 Subject: [PATCH 027/103] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AE=9A=E6=97=B6flu?= =?UTF-8?q?sh=20=E5=A4=B1=E8=B4=A5=E6=A0=87=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flink/sql/sink/rdb/format/JDBCUpsertOutputFormat.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/format/JDBCUpsertOutputFormat.java b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/format/JDBCUpsertOutputFormat.java index 4e047492f..b87c2e042 100644 --- a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/format/JDBCUpsertOutputFormat.java +++ b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/format/JDBCUpsertOutputFormat.java @@ -41,6 +41,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import static org.apache.flink.util.Preconditions.checkNotNull; @@ -76,6 +77,7 @@ public class JDBCUpsertOutputFormat extends AbstractJDBCOutputFormat tuple2) throws IOException { + if(!flushFlag.get()){ + throw new RuntimeException("connect exception,can not write record"); + } checkConnectionOpen(); try { if (outRecords.getCount() % RECEIVEDATA_PRINT_FREQUENTY == 0 || LOG.isDebugEnabled()) { @@ -187,6 +193,7 @@ private void checkConnectionOpen() { public synchronized void flush() throws Exception { jdbcWriter.executeBatch(connection); batchCount = 0; + flushFlag.set(true); } /** From 3b2f3648240113a895bb45e4c71de4e82045c844 Mon Sep 17 00:00:00 2001 From: dapeng Date: Fri, 18 Sep 2020 16:58:11 +0800 Subject: [PATCH 028/103] =?UTF-8?q?impala=20jdbc=20=E6=94=AF=E6=8C=81=20ch?= =?UTF-8?q?ar=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/dtstack/flink/sql/sink/rdb/JDBCTypeConvertUtils.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/JDBCTypeConvertUtils.java b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/JDBCTypeConvertUtils.java index ded966c68..b1a31de42 100644 --- a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/JDBCTypeConvertUtils.java +++ b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/JDBCTypeConvertUtils.java @@ -204,6 +204,8 @@ public static int[] buildSqlTypes(List fieldTypeArray) { tmpFieldsType[i] = Types.TINYINT; } else if (fieldType.equals(Short.class.getName())) { tmpFieldsType[i] = Types.SMALLINT; + } else if(fieldType.equals(Character.class.getName())){ + tmpFieldsType[i] = Types.CHAR; } else if (fieldType.equals(String.class.getName())) { tmpFieldsType[i] = Types.CHAR; } else if (fieldType.equals(Byte.class.getName())) { From f33d68ff856f02057215cf8cce9ead3c35261ffa Mon Sep 17 00:00:00 2001 From: wuren Date: Sat, 19 Sep 2020 15:55:59 +0800 Subject: [PATCH 029/103] remove add file statment --- .../dtstack/flink/sql/parser/SqlParser.java | 20 ++++++++ .../flink/sql/parser/SqlParserTest.java | 51 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 core/src/test/java/com/dtstack/flink/sql/parser/SqlParserTest.java diff --git a/core/src/main/java/com/dtstack/flink/sql/parser/SqlParser.java b/core/src/main/java/com/dtstack/flink/sql/parser/SqlParser.java index 23f8e4942..badcf8d9c 100644 --- a/core/src/main/java/com/dtstack/flink/sql/parser/SqlParser.java +++ b/core/src/main/java/com/dtstack/flink/sql/parser/SqlParser.java @@ -28,8 +28,11 @@ import com.google.common.collect.Lists; import com.google.common.base.Strings; +import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Reason: @@ -51,6 +54,8 @@ public static void setLocalSqlPluginRoot(String localSqlPluginRoot){ LOCAL_SQL_PLUGIN_ROOT = localSqlPluginRoot; } + private static final Pattern ADD_FIlE_PATTERN = Pattern.compile("(?i).*add\\s+file\\s+.+"); + /** * flink support sql syntax * CREATE TABLE sls_stream() with (); @@ -74,6 +79,7 @@ public static SqlTree parseSql(String sql) throws Exception { .replace("\t", " ").trim(); List sqlArr = DtStringUtil.splitIgnoreQuota(sql, SQL_DELIMITER); + sqlArr = removeAddFileStmt(sqlArr); SqlTree sqlTree = new SqlTree(); AbstractTableInfoParser tableInfoParser = new AbstractTableInfoParser(); for(String childSql : sqlArr){ @@ -154,4 +160,18 @@ public static SqlTree parseSql(String sql) throws Exception { return sqlTree; } + + /** + * remove add file with statment etc. add file /etc/krb5.conf; + */ + private static List removeAddFileStmt(List stmts) { + List cleanedStmts = new ArrayList<>(); + for (String stmt : stmts) { + Matcher matcher = ADD_FIlE_PATTERN.matcher(stmt); + if(!matcher.matches()) { + cleanedStmts.add(stmt); + } + } + return cleanedStmts; + } } diff --git a/core/src/test/java/com/dtstack/flink/sql/parser/SqlParserTest.java b/core/src/test/java/com/dtstack/flink/sql/parser/SqlParserTest.java new file mode 100644 index 000000000..fdbb03a75 --- /dev/null +++ b/core/src/test/java/com/dtstack/flink/sql/parser/SqlParserTest.java @@ -0,0 +1,51 @@ +/* + * 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.dtstack.flink.sql.parser; + +import org.junit.Assert; +import org.junit.Test; +import org.powermock.reflect.Whitebox; + +import java.util.ArrayList; +import java.util.List; + +/** + * @program: flink.sql + * @author: wuren + * @create: 2020/09/15 + **/ +public class SqlParserTest { + + @Test + public void testRemoveAddFileStmt() throws Exception { + List rawStmts = new ArrayList<>(); + String sql1 = " add file asdasdasd "; + String sql2 = " aDd fIle With asdasdasd "; + String sql3 = " INSERT INTO dwd_foo SELECT id, name FROM ods_foo"; + String sql4 = " ADD FILE asb "; + rawStmts.add(sql1); + rawStmts.add(sql2); + rawStmts.add(sql3); + rawStmts.add(sql4); + + List stmts = Whitebox.invokeMethod(SqlParser.class, "removeAddFileStmt", rawStmts); + Assert.assertEquals(stmts.get(0), sql3); + } + +} \ No newline at end of file From ad867fefd4b099cc06740dd52e96426965af9c7f Mon Sep 17 00:00:00 2001 From: dapeng Date: Mon, 21 Sep 2020 11:22:41 +0800 Subject: [PATCH 030/103] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E5=80=BC=E4=B8=8D=E5=AF=B9=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java b/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java index d4d1505f8..88332c21b 100644 --- a/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java +++ b/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java @@ -141,7 +141,7 @@ public void accept(Map values) { if (MapUtils.isNotEmpty(values)) { try { Row row = fillData(input.row(), values); - dealCacheData(key,CacheObj.buildCacheObj(ECacheContentType.SingleLine, row)); + dealCacheData(key,CacheObj.buildCacheObj(ECacheContentType.SingleLine, values)); resultFuture.complete(Collections.singleton(new CRow(row, input.change()))); } catch (Exception e) { dealFillDataError(input, resultFuture, e); From 4864dc79223c507d832b6f562dcd942993508659 Mon Sep 17 00:00:00 2001 From: wuren Date: Mon, 21 Sep 2020 15:00:54 +0800 Subject: [PATCH 031/103] fix hbase async with kafka kerberos login error --- .gitignore | 2 ++ .../sql/side/hbase/HbaseAsyncReqRow.java | 34 +++++++++++++++---- .../side/hbase/utils/HbaseConfigUtils.java | 13 ++----- .../{resource => resources}/log4j.properties | 0 4 files changed, 32 insertions(+), 17 deletions(-) rename launcher/src/main/{resource => resources}/log4j.properties (100%) diff --git a/.gitignore b/.gitignore index d7842e1ed..c8f97e79e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ lib/ bin/nohup.out .DS_Store bin/sideSql.txt +krb5.conf +*.keytab \ No newline at end of file diff --git a/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncReqRow.java b/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncReqRow.java index 0250decae..8d082a51a 100644 --- a/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncReqRow.java +++ b/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncReqRow.java @@ -31,10 +31,14 @@ import com.dtstack.flink.sql.factory.DTThreadFactory; import com.dtstack.flink.sql.side.hbase.utils.HbaseConfigUtils; import com.stumbleupon.async.Deferred; +import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; import org.apache.flink.api.java.typeutils.RowTypeInfo; import org.apache.flink.configuration.Configuration; +import org.apache.flink.runtime.security.DynamicConfiguration; +import org.apache.flink.runtime.security.KerberosUtils; import org.apache.flink.streaming.api.functions.async.ResultFuture; +import org.apache.hadoop.security.UserGroupInformation; import org.apache.flink.table.runtime.types.CRow; import org.apache.flink.table.typeutils.TimeIndicatorTypeInfo; import org.apache.flink.types.Row; @@ -45,7 +49,12 @@ import org.slf4j.LoggerFactory; import sun.security.krb5.KrbException; +import javax.security.auth.login.AppConfigurationEntry; +import java.io.File; +import java.lang.reflect.Field; +import java.security.PrivilegedExceptionAction; import java.sql.Timestamp; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TimeZone; @@ -100,7 +109,7 @@ public void open(Configuration parameters) throws Exception { ExecutorService executorService =new ThreadPoolExecutor(DEFAULT_POOL_SIZE, DEFAULT_POOL_SIZE, 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>(), new DTThreadFactory("hbase-aysnc")); + new LinkedBlockingQueue<>(), new DTThreadFactory("hbase-async")); Config config = new Config(); config.overrideConfig(HbaseConfigUtils.KEY_HBASE_ZOOKEEPER_QUORUM, hbaseSideTableInfo.getHost()); @@ -111,12 +120,15 @@ public void open(Configuration parameters) throws Exception { }); if (HbaseConfigUtils.asyncOpenKerberos(hbaseConfig)) { - String jaasStr = HbaseConfigUtils.buildJaasStr(hbaseConfig); - String jaasFilePath = HbaseConfigUtils.creatJassFile(jaasStr); - System.setProperty(HbaseConfigUtils.KEY_JAVA_SECURITY_AUTH_LOGIN_CONF, jaasFilePath); - config.overrideConfig(HbaseConfigUtils.KEY_JAVA_SECURITY_AUTH_LOGIN_CONF, jaasFilePath); + String principal = MapUtils.getString(hbaseConfig, HbaseConfigUtils.KEY_PRINCIPAL); + String keytab = System.getProperty("user.dir") + File.separator + MapUtils.getString(hbaseConfig, HbaseConfigUtils.KEY_KEY_TAB); + LOG.info("Kerberos login with keytab: {} and principal: {}", keytab, principal); + String name = "HBaseClient"; + config.overrideConfig("hbase.sasl.clientconfig", name); + appendJaasConf(name, keytab, principal); refreshConfig(); } + hBaseClient = new HBaseClient(config, executorService); try { @@ -144,11 +156,21 @@ public void open(Configuration parameters) throws Exception { } } + private void appendJaasConf(String name, String keytab, String principal) { + javax.security.auth.login.Configuration priorConfig = javax.security.auth.login.Configuration.getConfiguration(); + // construct a dynamic JAAS configuration + DynamicConfiguration currentConfig = new DynamicConfiguration(priorConfig); + // wire up the configured JAAS login contexts to use the krb5 entries + AppConfigurationEntry krb5Entry = KerberosUtils.keytabEntry(keytab, principal); + currentConfig.addAppConfigurationEntry(name, krb5Entry); + javax.security.auth.login.Configuration.setConfiguration(currentConfig); + } + private void refreshConfig() throws KrbException { sun.security.krb5.Config.refresh(); KerberosName.resetDefaultRealm(); //reload java.security.auth.login.config - javax.security.auth.login.Configuration.setConfiguration(null); +// javax.security.auth.login.Configuration.setConfiguration(null); } @Override diff --git a/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java b/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java index c6399e27a..c588b178b 100644 --- a/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java +++ b/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java @@ -33,7 +33,6 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.UUID; /** * @@ -60,7 +59,7 @@ public class HbaseConfigUtils { private final static String KEY_HBASE_SASL_CLIENTCONFIG = "hbase.sasl.clientconfig"; private final static String KEY_HBASE_KERBEROS_REGIONSERVER_PRINCIPAL = "hbase.kerberos.regionserver.principal"; public static final String KEY_KEY_TAB = "hbase.keytab"; - private static final String KEY_PRINCIPAL = "hbase.principal"; + public static final String KEY_PRINCIPAL = "hbase.principal"; public final static String KEY_HBASE_ZOOKEEPER_QUORUM = "hbase.zookeeper.quorum"; public final static String KEY_HBASE_ZOOKEEPER_ZNODE_QUORUM = "hbase.zookeeper.znode.parent"; @@ -115,9 +114,6 @@ public static boolean asyncOpenKerberos(Map hbaseConfigMap) { return AUTHENTICATION_TYPE.equalsIgnoreCase(MapUtils.getString(hbaseConfigMap, KEY_HBASE_SECURITY_AUTHENTICATION)); } - - - public static Configuration getHadoopConfiguration(Map hbaseConfigMap) { for (String key : KEYS_KERBEROS_REQUIRED) { if (StringUtils.isEmpty(MapUtils.getString(hbaseConfigMap, key))) { @@ -176,16 +172,13 @@ public static String creatJassFile(String configStr) throws IOException { return temp.getAbsolutePath(); } - public static String buildJaasStr(Map kerberosConfig) { + public static String buildJaasStr(Map kerberosConfig,String principal,String keyTab) { for (String key : ASYNC_KEYS_KERBEROS_REQUIRED) { if (StringUtils.isEmpty(MapUtils.getString(kerberosConfig, key))) { throw new IllegalArgumentException(String.format("Must provide [%s] when authentication is Kerberos", key)); } } - String keyTab = System.getProperty("user.dir") + File.separator + MapUtils.getString(kerberosConfig, KEY_KEY_TAB); - String principal = MapUtils.getString(kerberosConfig, KEY_PRINCIPAL); - StringBuilder jaasSB = new StringBuilder("Client {\n" + " com.sun.security.auth.module.Krb5LoginModule required\n" + " useKeyTab=true\n" + @@ -196,8 +189,6 @@ public static String buildJaasStr(Map kerberosConfig) { return jaasSB.toString(); } - - public static UserGroupInformation loginAndReturnUGI(Configuration conf, String principal, String keytab) throws IOException { if (conf == null) { throw new IllegalArgumentException("kerberos conf can not be null"); diff --git a/launcher/src/main/resource/log4j.properties b/launcher/src/main/resources/log4j.properties similarity index 100% rename from launcher/src/main/resource/log4j.properties rename to launcher/src/main/resources/log4j.properties From bf2e3095a6b1f77e46af04c66287a3f0b08dd283 Mon Sep 17 00:00:00 2001 From: tiezhu Date: Mon, 21 Sep 2020 15:51:37 +0800 Subject: [PATCH 032/103] [fix] fix hbase data dislocate --- .../dtstack/flink/sql/table/AbstractTableInfo.java | 2 +- .../flink/sql/sink/hbase/HbaseOutputFormat.java | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableInfo.java b/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableInfo.java index 40b7a7e82..f8fa9c3ef 100644 --- a/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableInfo.java +++ b/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableInfo.java @@ -49,7 +49,7 @@ public abstract class AbstractTableInfo implements Serializable { private final List fieldList = Lists.newArrayList(); /**key:别名, value: realField */ - private Map physicalFields = Maps.newHashMap(); + private Map physicalFields = Maps.newLinkedHashMap(); private final List fieldTypeList = Lists.newArrayList(); diff --git a/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/HbaseOutputFormat.java b/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/HbaseOutputFormat.java index 904c75a1b..6c31bd8dd 100644 --- a/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/HbaseOutputFormat.java +++ b/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/HbaseOutputFormat.java @@ -27,7 +27,11 @@ import org.apache.flink.configuration.Configuration; import org.apache.flink.types.Row; import org.apache.flink.util.Preconditions; -import org.apache.hadoop.hbase.*; +import org.apache.hadoop.hbase.AuthUtil; +import org.apache.hadoop.hbase.ChoreService; +import org.apache.hadoop.hbase.HBaseConfiguration; +import org.apache.hadoop.hbase.ScheduledChore; +import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.client.Connection; import org.apache.hadoop.hbase.client.ConnectionFactory; import org.apache.hadoop.hbase.client.Put; @@ -39,9 +43,9 @@ import java.io.File; import java.io.IOException; import java.security.PrivilegedAction; +import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Set; /** * @author: jingzhen@dtstack.com @@ -327,8 +331,8 @@ public HbaseOutputFormat finish() { String[] qualifiers = new String[format.columnNames.length]; if (format.columnNameFamily != null) { - Set keySet = format.columnNameFamily.keySet(); - String[] columns = keySet.toArray(new String[keySet.size()]); + List keyList = new LinkedList<>(format.columnNameFamily.keySet()); + String[] columns = keyList.toArray(new String[0]); for (int i = 0; i < columns.length; ++i) { String col = columns[i]; String[] part = col.split(":"); From e4082d2e25768bc6f678cb7a9a48d1f9533d709c Mon Sep 17 00:00:00 2001 From: tiezhu Date: Mon, 21 Sep 2020 16:27:46 +0800 Subject: [PATCH 033/103] [fix] fix hbase rowkey format defined --- .../java/com/dtstack/flink/sql/sink/hbase/RowKeyBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/RowKeyBuilder.java b/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/RowKeyBuilder.java index 7af51be3d..d0911dbcb 100644 --- a/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/RowKeyBuilder.java +++ b/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/RowKeyBuilder.java @@ -110,7 +110,7 @@ public static String[] splitIgnoreQuotaBrackets(String str, String delimiter){ public ReplaceInfo getReplaceInfo(String field){ field = field.trim(); - if(field.length() <= 2){ + if(field.length() <= 0){ throw new RuntimeException(field + " \n" + "Format defined exceptions"); } From 22dd152dfabeac6e07ef2a8a1def2fb06804b93f Mon Sep 17 00:00:00 2001 From: wuren Date: Mon, 21 Sep 2020 21:24:03 +0800 Subject: [PATCH 034/103] add error msg and remove useless method --- .../dtstack/flink/sql/util/DtFileUtils.java | 36 +++++++++++++++ .../sql/side/hbase/HbaseAsyncReqRow.java | 12 ++++- .../sql/side/hbase/HbaseAsyncSideInfo.java | 18 ++++++++ .../side/hbase/utils/HbaseConfigUtils.java | 44 +++---------------- 4 files changed, 70 insertions(+), 40 deletions(-) create mode 100644 core/src/main/java/com/dtstack/flink/sql/util/DtFileUtils.java diff --git a/core/src/main/java/com/dtstack/flink/sql/util/DtFileUtils.java b/core/src/main/java/com/dtstack/flink/sql/util/DtFileUtils.java new file mode 100644 index 000000000..eb1c974df --- /dev/null +++ b/core/src/main/java/com/dtstack/flink/sql/util/DtFileUtils.java @@ -0,0 +1,36 @@ +/* + * 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.dtstack.flink.sql.util; + +import org.apache.flink.util.Preconditions; + +import java.io.File; + +/** + * @program: flinkStreamSQL + * @author: wuren + * @create: 2020/09/21 + **/ +public class DtFileUtils { + public static void checkExists(String path) { + File file = new File(path); + String errorMsg = "%s file is not exist!"; + Preconditions.checkState(file.exists(), errorMsg, path); + } +} diff --git a/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncReqRow.java b/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncReqRow.java index 8d082a51a..f799f3439 100644 --- a/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncReqRow.java +++ b/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncReqRow.java @@ -30,6 +30,9 @@ import com.dtstack.flink.sql.side.hbase.table.HbaseSideTableInfo; import com.dtstack.flink.sql.factory.DTThreadFactory; import com.dtstack.flink.sql.side.hbase.utils.HbaseConfigUtils; +import com.dtstack.flink.sql.util.DtFileUtils; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.stumbleupon.async.Deferred; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; @@ -114,14 +117,21 @@ public void open(Configuration parameters) throws Exception { Config config = new Config(); config.overrideConfig(HbaseConfigUtils.KEY_HBASE_ZOOKEEPER_QUORUM, hbaseSideTableInfo.getHost()); config.overrideConfig(HbaseConfigUtils.KEY_HBASE_ZOOKEEPER_ZNODE_QUORUM, hbaseSideTableInfo.getParent()); - HbaseConfigUtils.loadKrb5Conf(hbaseConfig); hbaseConfig.entrySet().forEach(entity -> { config.overrideConfig(entity.getKey(), (String) entity.getValue()); }); if (HbaseConfigUtils.asyncOpenKerberos(hbaseConfig)) { + HbaseConfigUtils.loadKrb5Conf(hbaseConfig); + String principal = MapUtils.getString(hbaseConfig, HbaseConfigUtils.KEY_PRINCIPAL); + Preconditions.checkState(!Strings.isNullOrEmpty(principal), "%s must be set!", HbaseConfigUtils.KEY_PRINCIPAL); + String regionserver_principal = MapUtils.getString(hbaseConfig, HbaseConfigUtils.KEY_HBASE_KERBEROS_REGIONSERVER_PRINCIPAL); + Preconditions.checkState(!Strings.isNullOrEmpty(regionserver_principal), "%s must be set!", HbaseConfigUtils.KEY_HBASE_KERBEROS_REGIONSERVER_PRINCIPAL); + + MapUtils.getString(hbaseConfig, HbaseConfigUtils.KEY_KEY_TAB); String keytab = System.getProperty("user.dir") + File.separator + MapUtils.getString(hbaseConfig, HbaseConfigUtils.KEY_KEY_TAB); + DtFileUtils.checkExists(keytab); LOG.info("Kerberos login with keytab: {} and principal: {}", keytab, principal); String name = "HBaseClient"; config.overrideConfig("hbase.sasl.clientconfig", name); diff --git a/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncSideInfo.java b/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncSideInfo.java index 135aec004..6601458d4 100644 --- a/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncSideInfo.java +++ b/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncSideInfo.java @@ -1,3 +1,21 @@ +/* + * 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.dtstack.flink.sql.side.hbase; import com.dtstack.flink.sql.side.FieldInfo; diff --git a/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java b/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java index c588b178b..bdb4ffdf9 100644 --- a/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java +++ b/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java @@ -18,6 +18,7 @@ package com.dtstack.flink.sql.side.hbase.utils; +import com.dtstack.flink.sql.util.DtFileUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.conf.Configuration; @@ -26,9 +27,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.BufferedWriter; import java.io.File; -import java.io.FileWriter; import java.io.IOException; import java.util.Arrays; import java.util.List; @@ -57,7 +56,7 @@ public class HbaseConfigUtils { // async side kerberos private final static String KEY_HBASE_SECURITY_AUTH_ENABLE = "hbase.security.auth.enable"; private final static String KEY_HBASE_SASL_CLIENTCONFIG = "hbase.sasl.clientconfig"; - private final static String KEY_HBASE_KERBEROS_REGIONSERVER_PRINCIPAL = "hbase.kerberos.regionserver.principal"; + public final static String KEY_HBASE_KERBEROS_REGIONSERVER_PRINCIPAL = "hbase.kerberos.regionserver.principal"; public static final String KEY_KEY_TAB = "hbase.keytab"; public static final String KEY_PRINCIPAL = "hbase.principal"; @@ -100,13 +99,6 @@ public static Configuration getConfig(Map hbaseConfigMap) { return hConfiguration; } - public static boolean openKerberos(Map hbaseConfigMap) { - if (!MapUtils.getBooleanValue(hbaseConfigMap, KEY_HBASE_SECURITY_AUTHORIZATION)) { - return false; - } - return AUTHENTICATION_TYPE.equalsIgnoreCase(MapUtils.getString(hbaseConfigMap, KEY_HBASE_SECURITY_AUTHENTICATION)); - } - public static boolean asyncOpenKerberos(Map hbaseConfigMap) { if (!MapUtils.getBooleanValue(hbaseConfigMap, KEY_HBASE_SECURITY_AUTH_ENABLE)) { return false; @@ -156,39 +148,13 @@ public static String getKeytab(Map hbaseConfigMap) { public static void loadKrb5Conf(Map kerberosConfig) { String krb5FilePath = System.getProperty("user.dir") + File.separator + MapUtils.getString(kerberosConfig, KEY_JAVA_SECURITY_KRB5_CONF); + DtFileUtils.checkExists(krb5FilePath); if (!org.apache.commons.lang.StringUtils.isEmpty(krb5FilePath)) { - System.setProperty(KEY_JAVA_SECURITY_KRB5_CONF, krb5FilePath);; + System.setProperty(KEY_JAVA_SECURITY_KRB5_CONF, krb5FilePath); + LOG.info("{} is set to {}", KEY_JAVA_SECURITY_KRB5_CONF, krb5FilePath); } } - public static String creatJassFile(String configStr) throws IOException { - String fileName = System.getProperty("user.dir"); - File krbConf = new File(fileName); - File temp = File.createTempFile("JAAS", ".conf", krbConf); - temp.deleteOnExit(); - BufferedWriter out = new BufferedWriter(new FileWriter(temp, false)); - out.write(configStr + "\n"); - out.close(); - return temp.getAbsolutePath(); - } - - public static String buildJaasStr(Map kerberosConfig,String principal,String keyTab) { - for (String key : ASYNC_KEYS_KERBEROS_REQUIRED) { - if (StringUtils.isEmpty(MapUtils.getString(kerberosConfig, key))) { - throw new IllegalArgumentException(String.format("Must provide [%s] when authentication is Kerberos", key)); - } - } - - StringBuilder jaasSB = new StringBuilder("Client {\n" + - " com.sun.security.auth.module.Krb5LoginModule required\n" + - " useKeyTab=true\n" + - " useTicketCache=false\n"); - jaasSB.append(" keyTab=\"").append(keyTab).append("\"").append("\n"); - jaasSB.append(" principal=\"").append(principal).append("\"").append(";\n"); - jaasSB.append("};"); - return jaasSB.toString(); - } - public static UserGroupInformation loginAndReturnUGI(Configuration conf, String principal, String keytab) throws IOException { if (conf == null) { throw new IllegalArgumentException("kerberos conf can not be null"); From 215460328796d80ce2f07d62d7951e9b2bb411ed Mon Sep 17 00:00:00 2001 From: wuren Date: Mon, 21 Sep 2020 22:00:07 +0800 Subject: [PATCH 035/103] update krb5 error msg --- .../sql/side/hbase/utils/HbaseConfigUtils.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java b/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java index bdb4ffdf9..cdd48b640 100644 --- a/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java +++ b/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java @@ -19,6 +19,8 @@ package com.dtstack.flink.sql.side.hbase.utils; import com.dtstack.flink.sql.util.DtFileUtils; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.conf.Configuration; @@ -146,13 +148,13 @@ public static String getKeytab(Map hbaseConfigMap) { throw new IllegalArgumentException(""); } - public static void loadKrb5Conf(Map kerberosConfig) { - String krb5FilePath = System.getProperty("user.dir") + File.separator + MapUtils.getString(kerberosConfig, KEY_JAVA_SECURITY_KRB5_CONF); + public static void loadKrb5Conf(Map config) { + String krb5conf = MapUtils.getString(config, KEY_JAVA_SECURITY_KRB5_CONF); + Preconditions.checkState(!Strings.isNullOrEmpty(krb5conf), "%s must be set!", KEY_JAVA_SECURITY_KRB5_CONF); + String krb5FilePath = System.getProperty("user.dir") + File.separator + MapUtils.getString(config, KEY_JAVA_SECURITY_KRB5_CONF); DtFileUtils.checkExists(krb5FilePath); - if (!org.apache.commons.lang.StringUtils.isEmpty(krb5FilePath)) { - System.setProperty(KEY_JAVA_SECURITY_KRB5_CONF, krb5FilePath); - LOG.info("{} is set to {}", KEY_JAVA_SECURITY_KRB5_CONF, krb5FilePath); - } + System.setProperty(KEY_JAVA_SECURITY_KRB5_CONF, krb5FilePath); + LOG.info("{} is set to {}", KEY_JAVA_SECURITY_KRB5_CONF, krb5FilePath); } public static UserGroupInformation loginAndReturnUGI(Configuration conf, String principal, String keytab) throws IOException { From 28fa8c76fc18d5b99f5f2fd869941c1f7943aacd Mon Sep 17 00:00:00 2001 From: wuren Date: Tue, 22 Sep 2020 10:32:21 +0800 Subject: [PATCH 036/103] optmize code --- .../sql/side/hbase/HbaseAsyncReqRow.java | 16 +++++----- .../side/hbase/utils/HbaseConfigUtils.java | 31 ++++++++++++++++--- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncReqRow.java b/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncReqRow.java index f799f3439..c36c4affb 100644 --- a/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncReqRow.java +++ b/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/HbaseAsyncReqRow.java @@ -121,17 +121,17 @@ public void open(Configuration parameters) throws Exception { config.overrideConfig(entity.getKey(), (String) entity.getValue()); }); - if (HbaseConfigUtils.asyncOpenKerberos(hbaseConfig)) { + if (HbaseConfigUtils.isEnableKerberos(hbaseConfig)) { HbaseConfigUtils.loadKrb5Conf(hbaseConfig); - String principal = MapUtils.getString(hbaseConfig, HbaseConfigUtils.KEY_PRINCIPAL); - Preconditions.checkState(!Strings.isNullOrEmpty(principal), "%s must be set!", HbaseConfigUtils.KEY_PRINCIPAL); - String regionserver_principal = MapUtils.getString(hbaseConfig, HbaseConfigUtils.KEY_HBASE_KERBEROS_REGIONSERVER_PRINCIPAL); - Preconditions.checkState(!Strings.isNullOrEmpty(regionserver_principal), "%s must be set!", HbaseConfigUtils.KEY_HBASE_KERBEROS_REGIONSERVER_PRINCIPAL); + HbaseConfigUtils.checkOpt(principal, HbaseConfigUtils.KEY_PRINCIPAL); + String regionserverPrincipal = MapUtils.getString(hbaseConfig, HbaseConfigUtils.KEY_HBASE_KERBEROS_REGIONSERVER_PRINCIPAL); + HbaseConfigUtils.checkOpt(regionserverPrincipal, HbaseConfigUtils.KEY_HBASE_KERBEROS_REGIONSERVER_PRINCIPAL); + String keytab = MapUtils.getString(hbaseConfig, HbaseConfigUtils.KEY_KEY_TAB); + HbaseConfigUtils.checkOpt(keytab, HbaseConfigUtils.KEY_KEY_TAB); + String keytabPath = System.getProperty("user.dir") + File.separator + keytab; + DtFileUtils.checkExists(keytabPath); - MapUtils.getString(hbaseConfig, HbaseConfigUtils.KEY_KEY_TAB); - String keytab = System.getProperty("user.dir") + File.separator + MapUtils.getString(hbaseConfig, HbaseConfigUtils.KEY_KEY_TAB); - DtFileUtils.checkExists(keytab); LOG.info("Kerberos login with keytab: {} and principal: {}", keytab, principal); String name = "HBaseClient"; config.overrideConfig("hbase.sasl.clientconfig", name); diff --git a/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java b/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java index cdd48b640..cad4fccc9 100644 --- a/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java +++ b/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/utils/HbaseConfigUtils.java @@ -101,11 +101,27 @@ public static Configuration getConfig(Map hbaseConfigMap) { return hConfiguration; } - public static boolean asyncOpenKerberos(Map hbaseConfigMap) { - if (!MapUtils.getBooleanValue(hbaseConfigMap, KEY_HBASE_SECURITY_AUTH_ENABLE)) { - return false; + public static boolean isEnableKerberos(Map hbaseConfigMap) { + boolean hasAuthorization = AUTHENTICATION_TYPE.equalsIgnoreCase( + MapUtils.getString(hbaseConfigMap, KEY_HBASE_SECURITY_AUTHORIZATION) + ); + boolean hasAuthentication = AUTHENTICATION_TYPE.equalsIgnoreCase( + MapUtils.getString(hbaseConfigMap, KEY_HBASE_SECURITY_AUTHENTICATION) + ); + boolean hasAuthEnable = MapUtils.getBooleanValue(hbaseConfigMap, KEY_HBASE_SECURITY_AUTH_ENABLE); + + if(hasAuthentication || hasAuthorization || hasAuthEnable) { + LOG.info("Enable kerberos for hbase."); + setKerberosConf(hbaseConfigMap); + return true; } - return AUTHENTICATION_TYPE.equalsIgnoreCase(MapUtils.getString(hbaseConfigMap, KEY_HBASE_SECURITY_AUTHENTICATION)); + return false; + } + + private static void setKerberosConf(Map hbaseConfigMap) { + hbaseConfigMap.put(KEY_HBASE_SECURITY_AUTHORIZATION, AUTHENTICATION_TYPE); + hbaseConfigMap.put(KEY_HBASE_SECURITY_AUTHENTICATION, AUTHENTICATION_TYPE); + hbaseConfigMap.put(KEY_HBASE_SECURITY_AUTH_ENABLE, true); } public static Configuration getHadoopConfiguration(Map hbaseConfigMap) { @@ -150,13 +166,18 @@ public static String getKeytab(Map hbaseConfigMap) { public static void loadKrb5Conf(Map config) { String krb5conf = MapUtils.getString(config, KEY_JAVA_SECURITY_KRB5_CONF); - Preconditions.checkState(!Strings.isNullOrEmpty(krb5conf), "%s must be set!", KEY_JAVA_SECURITY_KRB5_CONF); + checkOpt(krb5conf, KEY_JAVA_SECURITY_KRB5_CONF); String krb5FilePath = System.getProperty("user.dir") + File.separator + MapUtils.getString(config, KEY_JAVA_SECURITY_KRB5_CONF); DtFileUtils.checkExists(krb5FilePath); System.setProperty(KEY_JAVA_SECURITY_KRB5_CONF, krb5FilePath); LOG.info("{} is set to {}", KEY_JAVA_SECURITY_KRB5_CONF, krb5FilePath); } + // TODO 日后改造可以下沉到Core模块 + public static void checkOpt(String opt, String key) { + Preconditions.checkState(!Strings.isNullOrEmpty(opt), "%s must be set!", key); + } + public static UserGroupInformation loginAndReturnUGI(Configuration conf, String principal, String keytab) throws IOException { if (conf == null) { throw new IllegalArgumentException("kerberos conf can not be null"); From bf17ce0568d0136c6c9a333a575730e086c32baa Mon Sep 17 00:00:00 2001 From: tiezhu Date: Tue, 22 Sep 2020 20:23:30 +0800 Subject: [PATCH 037/103] [fix] fix hbase rowkey format defined --- .../java/com/dtstack/flink/sql/side/hbase/RowKeyBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/RowKeyBuilder.java b/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/RowKeyBuilder.java index 6acfcb760..f80bf9f22 100644 --- a/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/RowKeyBuilder.java +++ b/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/RowKeyBuilder.java @@ -101,7 +101,7 @@ public static String[] splitIgnoreQuotaBrackets(String str, String delimiter){ public ReplaceInfo getReplaceInfo(String field){ field = field.trim(); - if(field.length() <= 2){ + if(field.length() <= 0){ throw new RuntimeException(field + " \n" + "Format defined exceptions"); } From e1f0662777282e5bf6138205f90c2ab459617173 Mon Sep 17 00:00:00 2001 From: tiezhu Date: Tue, 22 Sep 2020 22:13:17 +0800 Subject: [PATCH 038/103] =?UTF-8?q?[fix]=20fix=20=E7=BB=B4=E8=A1=A8join?= =?UTF-8?q?=E5=87=BA=E7=8E=B0Table=20not=20Found?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dtstack/flink/sql/side/JoinNodeDealer.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/side/JoinNodeDealer.java b/core/src/main/java/com/dtstack/flink/sql/side/JoinNodeDealer.java index 57104bd04..6afc12462 100644 --- a/core/src/main/java/com/dtstack/flink/sql/side/JoinNodeDealer.java +++ b/core/src/main/java/com/dtstack/flink/sql/side/JoinNodeDealer.java @@ -489,7 +489,7 @@ private Set extractSelectFieldFromJoinCondition(Set extractSelectFieldFromJoinCondition(Set fromTableNameSet, String checkTableName, Map mappingTableName) { + for (int i = 0; i < mappingTableName.size() + 1; i++) { + if (fromTableNameSet.contains(checkTableName)) { + return true; + } + + checkTableName = mappingTableName.get(checkTableName); + if (checkTableName == null) { + return false; + } + } + return true; + } + private Set extractFieldFromGroupByList(SqlNodeList parentGroupByList, Set fromTableNameSet, Map tableRef){ From 79e0842e796cd5aa0f454c3aa83564dd48aedd2b Mon Sep 17 00:00:00 2001 From: chuixue Date: Thu, 24 Sep 2020 09:31:49 +0800 Subject: [PATCH 039/103] [30589][kafka-sink]remove kafka sink retract data --- .../java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java | 1 + 1 file changed, 1 insertion(+) diff --git a/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java b/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java index 7234216a7..1d0c4d0e2 100644 --- a/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java +++ b/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java @@ -100,6 +100,7 @@ protected TableSchema buildTableSchema(String[] fieldNames, TypeInformation[] public void emitDataStream(DataStream> dataStream) { DataStream mapDataStream = dataStream .map((Tuple2 record) -> new CRow(record.f1, record.f0)) + .filter(x -> x.change()) .returns(getRowTypeInfo()) .setParallelism(parallelism); From c90af4d8de11d00b7d6667169f3079aa8a24dc1c Mon Sep 17 00:00:00 2001 From: tiezhu Date: Thu, 24 Sep 2020 17:26:42 +0800 Subject: [PATCH 040/103] [fix] sink add parallelism --- .../flink/sql/sink/cassandra/CassandraSink.java | 9 ++++++++- .../sql/sink/elasticsearch/ElasticsearchSink.java | 10 ++++++++-- .../elasticsearch/table/ElasticsearchSinkParser.java | 3 +++ .../sql/sink/elasticsearch/ElasticsearchSink.java | 10 ++++++++-- .../elasticsearch/table/ElasticsearchSinkParser.java | 3 +++ .../com/dtstack/flink/sql/sink/hbase/HbaseSink.java | 4 ++-- .../flink/sql/sink/kafka/AbstractKafkaSink.java | 2 +- .../flink/sql/sink/kafka/table/KafkaSinkParser.java | 2 +- .../com/dtstack/flink/sql/sink/kudu/KuduSink.java | 5 ++++- .../com/dtstack/flink/sql/sink/mongo/MongoSink.java | 11 ++++++++++- .../com/dtstack/flink/sql/sink/redis/RedisSink.java | 11 ++++++++++- .../flink/sql/sink/redis/table/RedisSinkParser.java | 5 +++-- 12 files changed, 61 insertions(+), 14 deletions(-) diff --git a/cassandra/cassandra-sink/src/main/java/com/dtstack/flink/sql/sink/cassandra/CassandraSink.java b/cassandra/cassandra-sink/src/main/java/com/dtstack/flink/sql/sink/cassandra/CassandraSink.java index 26152a7d3..a78ef97f7 100644 --- a/cassandra/cassandra-sink/src/main/java/com/dtstack/flink/sql/sink/cassandra/CassandraSink.java +++ b/cassandra/cassandra-sink/src/main/java/com/dtstack/flink/sql/sink/cassandra/CassandraSink.java @@ -34,6 +34,8 @@ import org.apache.flink.table.sinks.TableSink; import org.apache.flink.types.Row; +import java.util.Objects; + /** * Reason: * Date: 2018/11/22 @@ -57,6 +59,8 @@ public class CassandraSink implements RetractStreamTableSink, IStreamSinkGe protected Integer readTimeoutMillis; protected Integer connectTimeoutMillis; protected Integer poolTimeoutMillis; + protected Integer parallelism = 1; + protected String registerTableName; public CassandraSink() { // TO DO NOTHING @@ -77,6 +81,9 @@ public CassandraSink genStreamSink(AbstractTargetTableInfo targetTableInfo) { this.readTimeoutMillis = cassandraTableInfo.getReadTimeoutMillis(); this.connectTimeoutMillis = cassandraTableInfo.getConnectTimeoutMillis(); this.poolTimeoutMillis = cassandraTableInfo.getPoolTimeoutMillis(); + this.parallelism = Objects.isNull(cassandraTableInfo.getParallelism()) ? + parallelism : cassandraTableInfo.getParallelism(); + this.registerTableName = cassandraTableInfo.getTableName(); return this; } @@ -100,7 +107,7 @@ public void emitDataStream(DataStream> dataStream) { CassandraOutputFormat outputFormat = builder.finish(); RichSinkFunction richSinkFunction = new OutputFormatSinkFunction(outputFormat); - dataStream.addSink(richSinkFunction); + dataStream.addSink(richSinkFunction).setParallelism(parallelism).name(registerTableName); } @Override diff --git a/elasticsearch5/elasticsearch5-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/ElasticsearchSink.java b/elasticsearch5/elasticsearch5-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/ElasticsearchSink.java index 5ca81c5ed..6958804fa 100644 --- a/elasticsearch5/elasticsearch5-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/ElasticsearchSink.java +++ b/elasticsearch5/elasticsearch5-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/ElasticsearchSink.java @@ -45,6 +45,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; /** * table output elastic5plugin @@ -75,7 +76,9 @@ public class ElasticsearchSink implements RetractStreamTableSink, IStreamSi private TypeInformation[] fieldTypes; - private int parallelism = -1; + private int parallelism = 1; + + private String registerTableName; private ElasticsearchTableInfo esTableInfo; @@ -149,7 +152,7 @@ private RichSinkFunction createEsSinkFunction(){ @Override public void emitDataStream(DataStream> dataStream) { RichSinkFunction richSinkFunction = createEsSinkFunction(); - DataStreamSink streamSink = dataStream.addSink(richSinkFunction); + DataStreamSink streamSink = dataStream.addSink(richSinkFunction).name(registerTableName); if(parallelism > 0){ streamSink.setParallelism(parallelism); } @@ -176,6 +179,9 @@ public ElasticsearchSink genStreamSink(AbstractTargetTableInfo targetTableInfo) String id = elasticsearchTableInfo.getId(); String[] idField = StringUtils.split(id, ","); idIndexList = new ArrayList<>(); + parallelism = Objects.isNull(elasticsearchTableInfo.getParallelism()) ? + parallelism : elasticsearchTableInfo.getParallelism(); + registerTableName = elasticsearchTableInfo.getName(); for(int i = 0; i < idField.length; ++i) { idIndexList.add(Integer.valueOf(idField[i])); diff --git a/elasticsearch5/elasticsearch5-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/table/ElasticsearchSinkParser.java b/elasticsearch5/elasticsearch5-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/table/ElasticsearchSinkParser.java index 7988e597c..4117ad961 100644 --- a/elasticsearch5/elasticsearch5-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/table/ElasticsearchSinkParser.java +++ b/elasticsearch5/elasticsearch5-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/table/ElasticsearchSinkParser.java @@ -50,6 +50,8 @@ public class ElasticsearchSinkParser extends AbstractTableParser { private static final String KEY_ES_PASSWORD = "password"; + private static final String KEY_ES_PARALLELISM = "parallelism"; + @Override protected boolean fieldNameNeedsUpperCase() { return false; @@ -65,6 +67,7 @@ public AbstractTableInfo getTableInfo(String tableName, String fieldsInfo, Map, IStreamSi private TypeInformation[] fieldTypes; - private int parallelism = -1; + private int parallelism = 1; + + private String registerTableName; private ElasticsearchTableInfo esTableInfo; @@ -121,7 +124,7 @@ private RichSinkFunction createEsSinkFunction() { @Override public void emitDataStream(DataStream> dataStream) { RichSinkFunction richSinkFunction = createEsSinkFunction(); - DataStreamSink streamSink = dataStream.addSink(richSinkFunction); + DataStreamSink streamSink = dataStream.addSink(richSinkFunction).name(registerTableName); if (parallelism > 0) { streamSink.setParallelism(parallelism); } @@ -136,6 +139,9 @@ public ElasticsearchSink genStreamSink(AbstractTargetTableInfo targetTableInfo) columnTypes = esTableInfo.getFieldTypes(); esAddressList = Arrays.asList(esTableInfo.getAddress().split(",")); String id = esTableInfo.getId(); + parallelism = Objects.isNull(esTableInfo.getParallelism()) ? + parallelism : esTableInfo.getParallelism(); + registerTableName = esTableInfo.getName(); if (!StringUtils.isEmpty(id)) { idIndexList = Arrays.stream(StringUtils.split(id, ",")).map(Integer::valueOf).collect(Collectors.toList()); diff --git a/elasticsearch6/elasticsearch6-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/table/ElasticsearchSinkParser.java b/elasticsearch6/elasticsearch6-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/table/ElasticsearchSinkParser.java index 22c2b72bc..e1e26e1ac 100644 --- a/elasticsearch6/elasticsearch6-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/table/ElasticsearchSinkParser.java +++ b/elasticsearch6/elasticsearch6-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/table/ElasticsearchSinkParser.java @@ -49,6 +49,8 @@ public class ElasticsearchSinkParser extends AbstractTableParser { private static final String KEY_TRUE = "true"; + private static final String KEY_PARALLELISM = "parallelism"; + @Override protected boolean fieldNameNeedsUpperCase() { return false; @@ -64,6 +66,7 @@ public AbstractTableInfo getTableInfo(String tableName, String fieldsInfo, Map, IStreamSinkGener< private String clientPrincipal; private String clientKeytabFile; - private int parallelism = -1; + private int parallelism = 1; public HbaseSink() { @@ -115,7 +115,7 @@ public void emitDataStream(DataStream> dataStream) { HbaseOutputFormat outputFormat = builder.finish(); RichSinkFunction richSinkFunction = new OutputFormatSinkFunction(outputFormat); - dataStream.addSink(richSinkFunction); + dataStream.addSink(richSinkFunction).setParallelism(parallelism).name(registerTabName); } @Override diff --git a/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java b/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java index 7234216a7..427f5772c 100644 --- a/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java +++ b/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java @@ -56,7 +56,7 @@ public abstract class AbstractKafkaSink implements RetractStreamTableSink, protected String[] partitionKeys; protected String sinkOperatorName; protected Properties properties; - protected int parallelism; + protected int parallelism = 1; protected String topic; protected String tableName; diff --git a/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/table/KafkaSinkParser.java b/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/table/KafkaSinkParser.java index 4ad8947a8..58bf8bc78 100644 --- a/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/table/KafkaSinkParser.java +++ b/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/table/KafkaSinkParser.java @@ -57,7 +57,7 @@ public AbstractTableInfo getTableInfo(String tableName, String fieldsInfo, Map, Serializable, IStreamSinkGener { @@ -36,7 +37,7 @@ public class KuduSink implements RetractStreamTableSink, Serializable, IStr private Integer defaultSocketReadTimeoutMs; - private int parallelism = -1; + private int parallelism = 1; @Override public KuduSink genStreamSink(AbstractTargetTableInfo targetTableInfo) { @@ -47,6 +48,8 @@ public KuduSink genStreamSink(AbstractTargetTableInfo targetTableInfo) { this.defaultSocketReadTimeoutMs = kuduTableInfo.getDefaultSocketReadTimeoutMs(); this.workerCount = kuduTableInfo.getWorkerCount(); this.writeMode = kuduTableInfo.getWriteMode(); + this.parallelism = Objects.isNull(kuduTableInfo.getParallelism()) ? + parallelism : kuduTableInfo.getParallelism(); return this; } diff --git a/mongo/mongo-sink/src/main/java/com/dtstack/flink/sql/sink/mongo/MongoSink.java b/mongo/mongo-sink/src/main/java/com/dtstack/flink/sql/sink/mongo/MongoSink.java index 3f34055ec..39c12a789 100644 --- a/mongo/mongo-sink/src/main/java/com/dtstack/flink/sql/sink/mongo/MongoSink.java +++ b/mongo/mongo-sink/src/main/java/com/dtstack/flink/sql/sink/mongo/MongoSink.java @@ -33,6 +33,8 @@ import org.apache.flink.table.sinks.TableSink; import org.apache.flink.types.Row; +import java.util.Objects; + /** * Reason: * Date: 2018/11/6 @@ -48,6 +50,8 @@ public class MongoSink implements RetractStreamTableSink, IStreamSinkGener< protected String userName; protected String password; protected String database; + protected Integer parallelism = 1; + protected String registerTableName; public MongoSink() { // TO DO NOTHING @@ -61,6 +65,9 @@ public MongoSink genStreamSink(AbstractTargetTableInfo targetTableInfo) { this.userName = mongoTableInfo.getUserName(); this.password = mongoTableInfo.getPassword(); this.database = mongoTableInfo.getDatabase(); + this.parallelism = Objects.isNull(mongoTableInfo.getParallelism()) ? + parallelism : mongoTableInfo.getParallelism(); + this.registerTableName = mongoTableInfo.getName(); return this; } @@ -77,7 +84,9 @@ public void emitDataStream(DataStream> dataStream) { MongoOutputFormat outputFormat = builder.finish(); RichSinkFunction richSinkFunction = new OutputFormatSinkFunction(outputFormat); - dataStream.addSink(richSinkFunction); + dataStream.addSink(richSinkFunction) + .setParallelism(parallelism) + .name(registerTableName); } @Override diff --git a/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/RedisSink.java b/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/RedisSink.java index cc49a3ba8..f99e69725 100644 --- a/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/RedisSink.java +++ b/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/RedisSink.java @@ -33,6 +33,8 @@ import org.apache.flink.types.Row; import java.util.List; +import java.util.Objects; + /** * @author yanxi */ @@ -64,6 +66,10 @@ public class RedisSink implements RetractStreamTableSink, IStreamSinkGener< protected String masterName; + protected Integer parallelism = 1; + + protected String registerTableName; + public RedisSink(){ } @@ -82,6 +88,9 @@ public RedisSink genStreamSink(AbstractTargetTableInfo targetTableInfo) { this.minIdle = redisTableInfo.getMinIdle(); this.masterName = redisTableInfo.getMasterName(); this.timeout = redisTableInfo.getTimeout(); + this.parallelism = Objects.isNull(redisTableInfo.getParallelism()) ? + parallelism : redisTableInfo.getParallelism(); + this.registerTableName = redisTableInfo.getName(); return this; } @@ -108,7 +117,7 @@ public void emitDataStream(DataStream> dataStream) { .setMasterName(this.masterName); RedisOutputFormat redisOutputFormat = builder.finish(); RichSinkFunction richSinkFunction = new OutputFormatSinkFunction(redisOutputFormat); - dataStream.addSink(richSinkFunction); + dataStream.addSink(richSinkFunction).setParallelism(parallelism).name(registerTableName); } @Override diff --git a/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/table/RedisSinkParser.java b/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/table/RedisSinkParser.java index 8961f7da9..91e49c5fc 100644 --- a/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/table/RedisSinkParser.java +++ b/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/table/RedisSinkParser.java @@ -50,11 +50,12 @@ public AbstractTableInfo getTableInfo(String tableName, String fieldsInfo, Map primaryKeysList = Lists.newArrayList(); if (!StringUtils.isEmpty(primaryKeysStr)) { - List primaryKeysList = Lists.newArrayList(); primaryKeysList = Arrays.asList(StringUtils.split(primaryKeysStr, ",")); - redisTableInfo.setPrimaryKeys(primaryKeysList); } + redisTableInfo.setPrimaryKeys(primaryKeysList); + redisTableInfo.setParallelism(MathUtil.getIntegerVal(props.get(RedisTableInfo.PARALLELISM_KEY.toLowerCase()))); return redisTableInfo; } From 3d7cbb46e8feee135b759c1eb4c6466a799a2f28 Mon Sep 17 00:00:00 2001 From: tiezhu Date: Fri, 25 Sep 2020 11:22:28 +0800 Subject: [PATCH 041/103] [fix] fix when JOIN xxx then table not register error --- .../main/java/com/dtstack/flink/sql/util/TableUtils.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/src/main/java/com/dtstack/flink/sql/util/TableUtils.java b/core/src/main/java/com/dtstack/flink/sql/util/TableUtils.java index 2aeb06cf8..879b9204b 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/TableUtils.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/TableUtils.java @@ -232,6 +232,13 @@ public static String dealSelectResultWithJoinInfo(JoinInfo joinInfo, SqlSelect s queueInfo.offer(joinInfo.getLeftNode()); } + if (joinInfo.getLeftNode().getKind() == AS) { + SqlNode leftSqlNode = ((SqlBasicCall) joinInfo.getLeftNode()).getOperands()[0]; + if (leftSqlNode.getKind() == UNION) { + queueInfo.offer(joinInfo.getLeftNode()); + } + } + queueInfo.offer(joinInfo); } replaceFromNodeForJoin(joinInfo, sqlNode); From df6dc408ba92125eeed0ca5071c2df04201a3a84 Mon Sep 17 00:00:00 2001 From: chuixue Date: Sun, 27 Sep 2020 10:09:51 +0800 Subject: [PATCH 042/103] [30589][kafka-sink]remove kafka retract data in append mode --- .../com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java | 6 +++++- .../java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java | 1 + .../java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java | 1 + .../java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java | 1 + .../java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java | 1 + 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java b/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java index 1d0c4d0e2..55d1fa7f8 100644 --- a/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java +++ b/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java @@ -18,6 +18,7 @@ package com.dtstack.flink.sql.sink.kafka; +import com.dtstack.flink.sql.enums.EUpdateMode; import com.dtstack.flink.sql.sink.IStreamSinkGener; import com.dtstack.flink.sql.sink.kafka.table.KafkaSinkTableInfo; import org.apache.commons.lang3.StringUtils; @@ -59,6 +60,7 @@ public abstract class AbstractKafkaSink implements RetractStreamTableSink, protected int parallelism; protected String topic; protected String tableName; + protected String updateMode; protected TableSchema schema; protected SinkFunction kafkaProducer; @@ -98,9 +100,11 @@ protected TableSchema buildTableSchema(String[] fieldNames, TypeInformation[] @Override public void emitDataStream(DataStream> dataStream) { + if (updateMode.equalsIgnoreCase(EUpdateMode.APPEND.name())) { + dataStream = dataStream.filter((Tuple2 record) -> record.f0); + } DataStream mapDataStream = dataStream .map((Tuple2 record) -> new CRow(record.f1, record.f0)) - .filter(x -> x.change()) .returns(getRowTypeInfo()) .setParallelism(parallelism); diff --git a/kafka/kafka-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java b/kafka/kafka-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java index 632bb720e..3f92366b8 100644 --- a/kafka/kafka-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java +++ b/kafka/kafka-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java @@ -36,6 +36,7 @@ public KafkaSink genStreamSink(AbstractTargetTableInfo targetTableInfo) { Properties kafkaProperties = getKafkaProperties(kafkaSinkTableInfo); this.tableName = kafkaSinkTableInfo.getName(); + this.updateMode = kafkaSinkTableInfo.getUpdateMode(); this.topic = kafkaSinkTableInfo.getTopic(); this.partitioner = Optional.of(new CustomerFlinkPartition<>()); this.partitionKeys = getPartitionKeys(kafkaSinkTableInfo); diff --git a/kafka09/kafka09-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java b/kafka09/kafka09-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java index d22be3d59..402b6ed9b 100644 --- a/kafka09/kafka09-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java +++ b/kafka09/kafka09-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java @@ -38,6 +38,7 @@ public KafkaSink genStreamSink(AbstractTargetTableInfo targetTableInfo) { Properties kafkaProperties = getKafkaProperties(kafka09SinkTableInfo); this.tableName = kafka09SinkTableInfo.getName(); + this.updateMode = kafka09SinkTableInfo.getUpdateMode(); this.topic = kafka09SinkTableInfo.getTopic(); this.partitioner = Optional.of(new CustomerFlinkPartition<>()); this.partitionKeys = getPartitionKeys(kafka09SinkTableInfo); diff --git a/kafka10/kafka10-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java b/kafka10/kafka10-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java index eea78e121..09a1bd781 100644 --- a/kafka10/kafka10-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java +++ b/kafka10/kafka10-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java @@ -40,6 +40,7 @@ public KafkaSink genStreamSink(AbstractTargetTableInfo targetTableInfo) { Properties kafkaProperties = getKafkaProperties(kafka10SinkTableInfo); this.tableName = kafka10SinkTableInfo.getName(); + this.updateMode = kafka10SinkTableInfo.getUpdateMode(); this.topic = kafka10SinkTableInfo.getTopic(); this.partitioner = Optional.of(new CustomerFlinkPartition<>()); this.partitionKeys = getPartitionKeys(kafka10SinkTableInfo); diff --git a/kafka11/kafka11-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java b/kafka11/kafka11-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java index ea45280c7..eee1a4ce5 100644 --- a/kafka11/kafka11-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java +++ b/kafka11/kafka11-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/KafkaSink.java @@ -41,6 +41,7 @@ public KafkaSink genStreamSink(AbstractTargetTableInfo targetTableInfo) { Properties kafkaProperties = getKafkaProperties(kafka11SinkTableInfo); this.tableName = kafka11SinkTableInfo.getName(); + this.updateMode = kafka11SinkTableInfo.getUpdateMode(); this.topic = kafka11SinkTableInfo.getTopic(); this.partitioner = Optional.of(new CustomerFlinkPartition<>()); this.partitionKeys = getPartitionKeys(kafka11SinkTableInfo); From 1a05ab141fd76031764e6c18006dfed042ed24ba Mon Sep 17 00:00:00 2001 From: dapeng Date: Sun, 27 Sep 2020 16:59:52 +0800 Subject: [PATCH 043/103] =?UTF-8?q?elasticsearch=20bulk=20=E7=9A=84?= =?UTF-8?q?=E5=A4=A7=E5=B0=8F=E5=86=99=E6=AD=BB=E4=BA=861?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sql/sink/elasticsearch/ElasticsearchSink.java | 1 + .../elasticsearch/table/ElasticsearchSinkParser.java | 3 +++ .../elasticsearch/table/ElasticsearchTableInfo.java | 10 ++++++++++ 3 files changed, 14 insertions(+) diff --git a/elasticsearch6/elasticsearch6-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/ElasticsearchSink.java b/elasticsearch6/elasticsearch6-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/ElasticsearchSink.java index b7d9de6fc..602d4cfba 100644 --- a/elasticsearch6/elasticsearch6-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/ElasticsearchSink.java +++ b/elasticsearch6/elasticsearch6-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/ElasticsearchSink.java @@ -135,6 +135,7 @@ public ElasticsearchSink genStreamSink(AbstractTargetTableInfo targetTableInfo) type = esTableInfo.getEsType(); columnTypes = esTableInfo.getFieldTypes(); esAddressList = Arrays.asList(esTableInfo.getAddress().split(",")); + this.bulkFlushMaxActions = esTableInfo.getBatchSize(); String id = esTableInfo.getId(); if (!StringUtils.isEmpty(id)) { diff --git a/elasticsearch6/elasticsearch6-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/table/ElasticsearchSinkParser.java b/elasticsearch6/elasticsearch6-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/table/ElasticsearchSinkParser.java index 22c2b72bc..560872b05 100644 --- a/elasticsearch6/elasticsearch6-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/table/ElasticsearchSinkParser.java +++ b/elasticsearch6/elasticsearch6-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/table/ElasticsearchSinkParser.java @@ -47,6 +47,8 @@ public class ElasticsearchSinkParser extends AbstractTableParser { private static final String KEY_ES6_PASSWORD = "password"; + public static final String BATCH_SIZE_KEY = "batchSize"; + private static final String KEY_TRUE = "true"; @Override @@ -64,6 +66,7 @@ public AbstractTableInfo getTableInfo(String tableName, String fieldsInfo, Map Date: Sun, 27 Sep 2020 17:56:46 +0800 Subject: [PATCH 044/103] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dredis=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E4=B8=8D=E8=83=BD=E5=8C=85=E5=90=AB#=EF=BC=8C?= =?UTF-8?q?=EF=BC=9F=E7=AD=89=E7=89=B9=E6=AE=8A=E5=AD=97=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flink/sql/side/redis/RedisAsyncReqRow.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java b/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java index d4d1505f8..e0a348c38 100644 --- a/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java +++ b/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java @@ -21,7 +21,9 @@ import com.dtstack.flink.sql.side.AbstractSideTableInfo; import com.dtstack.flink.sql.side.BaseAsyncReqRow; import io.lettuce.core.KeyValue; +import io.lettuce.core.api.async.RedisAsyncCommands; import io.lettuce.core.api.async.RedisStringAsyncCommands; +import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; import org.apache.flink.api.java.typeutils.RowTypeInfo; import org.apache.flink.configuration.Configuration; import org.apache.flink.streaming.api.functions.async.ResultFuture; @@ -87,11 +89,6 @@ public void open(Configuration parameters) throws Exception { private void buildRedisClient(RedisSideTableInfo tableInfo){ String url = redisSideTableInfo.getUrl(); String password = redisSideTableInfo.getPassword(); - if (password != null){ - password = password + "@"; - } else { - password = ""; - } String database = redisSideTableInfo.getDatabase(); if (database == null){ database = "0"; @@ -99,25 +96,28 @@ private void buildRedisClient(RedisSideTableInfo tableInfo){ switch (RedisType.parse(tableInfo.getRedisType())){ case STANDALONE: StringBuilder redisUri = new StringBuilder(); - redisUri.append("redis://").append(password).append(url).append("/").append(database); + redisUri.append("redis://").append(url).append("/").append(database); redisClient = RedisClient.create(redisUri.toString()); connection = redisClient.connect(); async = connection.async(); + ((RedisAsyncCommands)async).auth(password); break; case SENTINEL: StringBuilder sentinelUri = new StringBuilder(); - sentinelUri.append("redis-sentinel://").append(password) + sentinelUri.append("redis-sentinel://") .append(url).append("/").append(database).append("#").append(redisSideTableInfo.getMasterName()); redisClient = RedisClient.create(sentinelUri.toString()); connection = redisClient.connect(); async = connection.async(); + ((RedisAsyncCommands)async).auth(password); break; case CLUSTER: StringBuilder clusterUri = new StringBuilder(); - clusterUri.append("redis://").append(password).append(url); + clusterUri.append("redis://").append(url); clusterClient = RedisClusterClient.create(clusterUri.toString()); clusterConnection = clusterClient.connect(); async = clusterConnection.async(); + ((RedisAdvancedClusterAsyncCommands)async).auth(password); default: break; } From 09f97a9c9389e0bff756f3dd5aaea477832b68b8 Mon Sep 17 00:00:00 2001 From: zoudaokoulife Date: Tue, 29 Sep 2020 23:03:52 +0800 Subject: [PATCH 045/103] =?UTF-8?q?=E4=BF=AE=E6=94=B94.1.X=20=E5=AF=B9?= =?UTF-8?q?=E5=BA=94=E5=88=B0flink=E7=89=88=E6=9C=AC=E5=8F=B7=E5=88=B01.8.?= =?UTF-8?q?1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0496c6dd8..374ec3c31 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ UTF-8 - 1.8.3 + 1.8.1 From 64b681fb6f4396d879304d00e94165be21a87666 Mon Sep 17 00:00:00 2001 From: wuren Date: Fri, 9 Oct 2020 10:08:47 +0800 Subject: [PATCH 046/103] remove add file statment --- .../dtstack/flink/sql/parser/SqlParser.java | 20 ++++++++ .../flink/sql/parser/SqlParserTest.java | 51 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 core/src/test/java/com/dtstack/flink/sql/parser/SqlParserTest.java diff --git a/core/src/main/java/com/dtstack/flink/sql/parser/SqlParser.java b/core/src/main/java/com/dtstack/flink/sql/parser/SqlParser.java index 23f8e4942..badcf8d9c 100644 --- a/core/src/main/java/com/dtstack/flink/sql/parser/SqlParser.java +++ b/core/src/main/java/com/dtstack/flink/sql/parser/SqlParser.java @@ -28,8 +28,11 @@ import com.google.common.collect.Lists; import com.google.common.base.Strings; +import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Reason: @@ -51,6 +54,8 @@ public static void setLocalSqlPluginRoot(String localSqlPluginRoot){ LOCAL_SQL_PLUGIN_ROOT = localSqlPluginRoot; } + private static final Pattern ADD_FIlE_PATTERN = Pattern.compile("(?i).*add\\s+file\\s+.+"); + /** * flink support sql syntax * CREATE TABLE sls_stream() with (); @@ -74,6 +79,7 @@ public static SqlTree parseSql(String sql) throws Exception { .replace("\t", " ").trim(); List sqlArr = DtStringUtil.splitIgnoreQuota(sql, SQL_DELIMITER); + sqlArr = removeAddFileStmt(sqlArr); SqlTree sqlTree = new SqlTree(); AbstractTableInfoParser tableInfoParser = new AbstractTableInfoParser(); for(String childSql : sqlArr){ @@ -154,4 +160,18 @@ public static SqlTree parseSql(String sql) throws Exception { return sqlTree; } + + /** + * remove add file with statment etc. add file /etc/krb5.conf; + */ + private static List removeAddFileStmt(List stmts) { + List cleanedStmts = new ArrayList<>(); + for (String stmt : stmts) { + Matcher matcher = ADD_FIlE_PATTERN.matcher(stmt); + if(!matcher.matches()) { + cleanedStmts.add(stmt); + } + } + return cleanedStmts; + } } diff --git a/core/src/test/java/com/dtstack/flink/sql/parser/SqlParserTest.java b/core/src/test/java/com/dtstack/flink/sql/parser/SqlParserTest.java new file mode 100644 index 000000000..fdbb03a75 --- /dev/null +++ b/core/src/test/java/com/dtstack/flink/sql/parser/SqlParserTest.java @@ -0,0 +1,51 @@ +/* + * 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.dtstack.flink.sql.parser; + +import org.junit.Assert; +import org.junit.Test; +import org.powermock.reflect.Whitebox; + +import java.util.ArrayList; +import java.util.List; + +/** + * @program: flink.sql + * @author: wuren + * @create: 2020/09/15 + **/ +public class SqlParserTest { + + @Test + public void testRemoveAddFileStmt() throws Exception { + List rawStmts = new ArrayList<>(); + String sql1 = " add file asdasdasd "; + String sql2 = " aDd fIle With asdasdasd "; + String sql3 = " INSERT INTO dwd_foo SELECT id, name FROM ods_foo"; + String sql4 = " ADD FILE asb "; + rawStmts.add(sql1); + rawStmts.add(sql2); + rawStmts.add(sql3); + rawStmts.add(sql4); + + List stmts = Whitebox.invokeMethod(SqlParser.class, "removeAddFileStmt", rawStmts); + Assert.assertEquals(stmts.get(0), sql3); + } + +} \ No newline at end of file From badc105562e054dc94664f1f03d019e36f1c93f1 Mon Sep 17 00:00:00 2001 From: wuren Date: Fri, 9 Oct 2020 10:54:31 +0800 Subject: [PATCH 047/103] add unit test dependence --- pom.xml | 53 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 0496c6dd8..d10954d3e 100644 --- a/pom.xml +++ b/pom.xml @@ -39,11 +39,56 @@ + + UTF-8 + 1.8.3 + 2.7.3 + 4.12 + 2.21.0 + 2.0.4 + 0.7.8 + - - UTF-8 - 1.8.3 - + + + + junit + junit + ${junit.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.powermock + powermock-module-junit4 + ${powermock.version} + test + + + org.powermock + powermock-api-mockito2 + ${powermock.version} + test + + + org.mockito + mockito-core + + + + + org.jacoco + org.jacoco.agent + runtime + test + ${jacoco.version} + + From a786cf8b622a729ccaac594b283a251a16ad1ab5 Mon Sep 17 00:00:00 2001 From: wuren Date: Fri, 9 Oct 2020 13:55:15 +0800 Subject: [PATCH 048/103] add unit test dependences --- pom.xml | 53 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 374ec3c31..d10954d3e 100644 --- a/pom.xml +++ b/pom.xml @@ -39,11 +39,56 @@ + + UTF-8 + 1.8.3 + 2.7.3 + 4.12 + 2.21.0 + 2.0.4 + 0.7.8 + - - UTF-8 - 1.8.1 - + + + + junit + junit + ${junit.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.powermock + powermock-module-junit4 + ${powermock.version} + test + + + org.powermock + powermock-api-mockito2 + ${powermock.version} + test + + + org.mockito + mockito-core + + + + + org.jacoco + org.jacoco.agent + runtime + test + ${jacoco.version} + + From ea2d6431db113d0d7122edeeab0a55e6f7762705 Mon Sep 17 00:00:00 2001 From: tiezhu Date: Sat, 10 Oct 2020 12:42:28 +0800 Subject: [PATCH 049/103] resolve conflict --- .../java/com/dtstack/flink/sql/side/hbase/RowKeyBuilder.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/RowKeyBuilder.java b/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/RowKeyBuilder.java index 6acfcb760..1269b2844 100644 --- a/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/RowKeyBuilder.java +++ b/hbase/hbase-side/hbase-side-core/src/main/java/com/dtstack/flink/sql/side/hbase/RowKeyBuilder.java @@ -101,10 +101,6 @@ public static String[] splitIgnoreQuotaBrackets(String str, String delimiter){ public ReplaceInfo getReplaceInfo(String field){ field = field.trim(); - if(field.length() <= 2){ - throw new RuntimeException(field + " \n" + - "Format defined exceptions"); - } //判断是不是常量==>''包裹的标识 if(field.startsWith("'") && field.endsWith("'")){ From 3f7488d0120439e0045e26388bfb4c6a19326f79 Mon Sep 17 00:00:00 2001 From: tiezhu Date: Sat, 10 Oct 2020 15:00:58 +0800 Subject: [PATCH 050/103] =?UTF-8?q?[fix]=20Flink=E4=B8=8D=E6=94=AF?= =?UTF-8?q?=E6=8C=81text=EF=BC=8C=E5=AF=B9=E7=94=A8=E6=88=B7=E5=81=9A?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/main/java/com/dtstack/flink/sql/util/ClassUtil.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/util/ClassUtil.java b/core/src/main/java/com/dtstack/flink/sql/util/ClassUtil.java index ec88bdbe9..21551364f 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/ClassUtil.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/ClassUtil.java @@ -68,9 +68,10 @@ public static Class stringConvertClass(String str) { case "varchar": case "char": - case "text": case "string": return String.class; + case "text": + throw new IllegalArgumentException(str + " type is not support, please use STRING. "); case "real": case "float": From c8a367483cb25dde581588281686a982264ef64e Mon Sep 17 00:00:00 2001 From: tiezhu Date: Sat, 10 Oct 2020 15:36:59 +0800 Subject: [PATCH 051/103] =?UTF-8?q?[fix]=20Flink=E4=B8=8D=E6=94=AF?= =?UTF-8?q?=E6=8C=81text=EF=BC=8C=E5=AF=B9=E7=94=A8=E6=88=B7=E5=81=9A?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../side/elasticsearch6/util/ClassUtil.java | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/ClassUtil.java b/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/ClassUtil.java index 90ac58229..4dd1c4300 100644 --- a/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/ClassUtil.java +++ b/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/ClassUtil.java @@ -15,12 +15,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - - package com.dtstack.flink.sql.side.elasticsearch6.util; import com.dtstack.flink.sql.util.DateUtil; +import java.lang.reflect.Array; import java.math.BigDecimal; import java.sql.Date; import java.sql.Time; @@ -30,13 +29,19 @@ * Reason: TODO ADD REASON(可选) * Date: 2017年03月10日 下午1:16:37 * Company: www.dtstack.com - * * @author sishu.yss */ public class ClassUtil { public static Class stringConvertClass(String str) { - switch (str.toLowerCase()) { + + // 这部分主要是告诉Class转TypeInfomation的方法,字段是Array类型 + String lowerStr = str.toLowerCase().trim(); + if (lowerStr.startsWith("array")) { + return Array.newInstance(Integer.class, 0).getClass(); + } + + switch (lowerStr) { case "boolean": case "bit": return Boolean.class; @@ -54,7 +59,6 @@ public static Class stringConvertClass(String str) { case "blob": return Byte.class; - case "long": case "bigint": case "intunsigned": case "integerunsigned": @@ -63,8 +67,10 @@ public static Class stringConvertClass(String str) { case "varchar": case "char": - case "text": + case "string": return String.class; + case "text": + throw new IllegalArgumentException(str + " type is not support, please use STRING. "); case "real": case "float": @@ -90,9 +96,10 @@ public static Class stringConvertClass(String str) { case "decimalunsigned": return BigDecimal.class; default: - throw new RuntimeException("不支持 " + str + " 类型"); + break; } + throw new RuntimeException("不支持 " + str + " 类型"); } public static Object convertType(Object field, String fromType, String toType) { From 1284859eced6f93556b40feef8284d283fc82539 Mon Sep 17 00:00:00 2001 From: wuren Date: Tue, 13 Oct 2020 09:58:15 +0800 Subject: [PATCH 052/103] rollback flink version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d10954d3e..d21ae0f9a 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ UTF-8 - 1.8.3 + 1.8.1 2.7.3 4.12 2.21.0 From 42c7c18c23d60ce783c5b257a23bf96797711814 Mon Sep 17 00:00:00 2001 From: dapeng Date: Tue, 13 Oct 2020 20:56:36 +0800 Subject: [PATCH 053/103] =?UTF-8?q?=E5=8D=87=E7=BA=A7flink=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E8=87=B31.8.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d21ae0f9a..d10954d3e 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ UTF-8 - 1.8.1 + 1.8.3 2.7.3 4.12 2.21.0 From d87145c8fc582ec87665ff4ba103d532dbe2a372 Mon Sep 17 00:00:00 2001 From: dapeng Date: Wed, 21 Oct 2020 10:32:34 +0800 Subject: [PATCH 054/103] =?UTF-8?q?fix=20=E8=BF=9E=E6=8E=A5=E6=96=AD?= =?UTF-8?q?=E5=BC=80=E4=BB=BB=E5=8A=A1=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sql/sink/rdb/writer/AbstractUpsertWriter.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AbstractUpsertWriter.java b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AbstractUpsertWriter.java index 42ed545a6..bc4f763a5 100644 --- a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AbstractUpsertWriter.java +++ b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AbstractUpsertWriter.java @@ -170,8 +170,16 @@ public void executeUpdate(Connection connection) throws SQLException { connection.commit(); } catch (Exception e) { // deal pg error: current transaction is aborted, commands ignored until end of transaction block - connection.rollback(); - connection.commit(); + try { + connection.rollback(); + connection.commit(); + } catch (SQLException e1) { + throw new RuntimeException(e1); + } + + if(e.getMessage().contains("doesn't exist")){ + throw new RuntimeException(e); + } if (metricOutputFormat.outDirtyRecords.getCount() % DIRTYDATA_PRINT_FREQUENTY == 0 || LOG.isDebugEnabled()) { LOG.error("record insert failed ,this row is {}", entry.getValue()); LOG.error("", e); From ae223316115642e075a6db8169817d14c2603df2 Mon Sep 17 00:00:00 2001 From: dapeng Date: Wed, 21 Oct 2020 11:39:03 +0800 Subject: [PATCH 055/103] =?UTF-8?q?fix=20=E7=89=88=E6=9C=AC=E4=B8=8D?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E7=9A=84api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sql/side/redis/RedisAsyncReqRow.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java b/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java index e0a348c38..c0d418fc1 100644 --- a/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java +++ b/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java @@ -21,6 +21,7 @@ import com.dtstack.flink.sql.side.AbstractSideTableInfo; import com.dtstack.flink.sql.side.BaseAsyncReqRow; import io.lettuce.core.KeyValue; +import io.lettuce.core.RedisURI; import io.lettuce.core.api.async.RedisAsyncCommands; import io.lettuce.core.api.async.RedisStringAsyncCommands; import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; @@ -95,29 +96,28 @@ private void buildRedisClient(RedisSideTableInfo tableInfo){ } switch (RedisType.parse(tableInfo.getRedisType())){ case STANDALONE: - StringBuilder redisUri = new StringBuilder(); - redisUri.append("redis://").append(url).append("/").append(database); - redisClient = RedisClient.create(redisUri.toString()); + RedisURI redisURI = RedisURI.create("redis://" + url); + redisURI.setPassword(password); + redisURI.setDatabase(Integer.valueOf(database)); + redisClient = RedisClient.create(redisURI); connection = redisClient.connect(); async = connection.async(); - ((RedisAsyncCommands)async).auth(password); break; case SENTINEL: - StringBuilder sentinelUri = new StringBuilder(); - sentinelUri.append("redis-sentinel://") - .append(url).append("/").append(database).append("#").append(redisSideTableInfo.getMasterName()); - redisClient = RedisClient.create(sentinelUri.toString()); + RedisURI redisSentinelURI = RedisURI.create("redis-sentinel://" + url); + redisSentinelURI.setPassword(password); + redisSentinelURI.setDatabase(Integer.valueOf(database)); + redisSentinelURI.setSentinelMasterId(redisSideTableInfo.getMasterName()); + redisClient = RedisClient.create(redisSentinelURI); connection = redisClient.connect(); async = connection.async(); - ((RedisAsyncCommands)async).auth(password); break; case CLUSTER: - StringBuilder clusterUri = new StringBuilder(); - clusterUri.append("redis://").append(url); - clusterClient = RedisClusterClient.create(clusterUri.toString()); + RedisURI clusterURI = RedisURI.create("redis://" + url); + clusterURI.setPassword(password); + clusterClient = RedisClusterClient.create(clusterURI); clusterConnection = clusterClient.connect(); async = clusterConnection.async(); - ((RedisAdvancedClusterAsyncCommands)async).auth(password); default: break; } From 44858b19b1190f2baf203ad3d9d4c8da4cb31938 Mon Sep 17 00:00:00 2001 From: tiezhu Date: Mon, 26 Oct 2020 09:36:38 +0800 Subject: [PATCH 056/103] [feat] impala and kudu with kerberos [feat] impala support dynamic and static partition [feat] complete kudu with kerberos [fix] import and override error [feat] impala with kerberos --- .../sql/constrant/PluginParamConsts.java | 30 + .../dtstack/flink/sql/krb/KerberosTable.java | 65 ++ .../flink/sql/side/BaseAsyncReqRow.java | 7 +- .../com/dtstack/flink/sql/util/KrbUtils.java | 54 ++ .../dtstack/flink/sql/util/KrbUtilsTest.java | 44 + docs/plugin/kuduSide.md | 22 + docs/plugin/kuduSink.md | 149 +++- .../sql/side/impala/ImpalaAllReqRow.java | 37 +- .../sql/side/impala/ImpalaAsyncReqRow.java | 79 +- .../sql/sink/impala/ImpalaOutputFormat.java | 754 ++++++++++++++++++ .../flink/sql/sink/impala/ImpalaSink.java | 206 +++-- .../sink/impala/table/ImpalaSinkParser.java | 150 ++-- .../sink/impala/table/ImpalaTableInfo.java | 172 +++- .../flink/sql/side/kudu/KuduAllReqRow.java | 52 +- .../flink/sql/side/kudu/KuduAsyncReqRow.java | 57 +- .../sql/side/kudu/table/KuduSideParser.java | 13 + .../side/kudu/table/KuduSideTableInfo.java | 51 +- .../flink/sql/sink/kudu/KuduOutputFormat.java | 52 +- .../dtstack/flink/sql/sink/kudu/KuduSink.java | 20 +- .../sql/sink/kudu/table/KuduSinkParser.java | 13 + .../sql/sink/kudu/table/KuduTableInfo.java | 53 +- .../sql/side/rdb/async/RdbAsyncReqRow.java | 112 ++- .../sql/sink/rdb/JDBCTypeConvertUtils.java | 55 ++ .../rdb/format/JDBCUpsertOutputFormat.java | 16 +- 24 files changed, 1965 insertions(+), 298 deletions(-) create mode 100644 core/src/main/java/com/dtstack/flink/sql/constrant/PluginParamConsts.java create mode 100644 core/src/main/java/com/dtstack/flink/sql/krb/KerberosTable.java create mode 100644 core/src/main/java/com/dtstack/flink/sql/util/KrbUtils.java create mode 100644 core/src/test/java/com/dtstack/flink/sql/util/KrbUtilsTest.java create mode 100644 impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java diff --git a/core/src/main/java/com/dtstack/flink/sql/constrant/PluginParamConsts.java b/core/src/main/java/com/dtstack/flink/sql/constrant/PluginParamConsts.java new file mode 100644 index 000000000..baf314c19 --- /dev/null +++ b/core/src/main/java/com/dtstack/flink/sql/constrant/PluginParamConsts.java @@ -0,0 +1,30 @@ +/* + * 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.dtstack.flink.sql.constrant; + +/** + * @program: flinkStreamSQL + * @author: wuren + * @create: 2020/09/15 + **/ +public class PluginParamConsts { + public static final String PRINCIPAL = "principal"; + public static final String KEYTAB = "keytab"; + public static final String KRB5_CONF = "krb5conf"; +} diff --git a/core/src/main/java/com/dtstack/flink/sql/krb/KerberosTable.java b/core/src/main/java/com/dtstack/flink/sql/krb/KerberosTable.java new file mode 100644 index 000000000..ce6691294 --- /dev/null +++ b/core/src/main/java/com/dtstack/flink/sql/krb/KerberosTable.java @@ -0,0 +1,65 @@ +/* + * 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.dtstack.flink.sql.krb; + +import com.google.common.base.Strings; + +/** + * @program: flinkStreamSQL + * @author: wuren + * @create: 2020/09/15 + **/ +public interface KerberosTable { + + String getPrincipal(); + + void setPrincipal(String principal); + + String getKeytab(); + + void setKeytab(String keytab); + + String getKrb5conf(); + + void setKrb5conf(String krb5conf); + + boolean isEnableKrb(); + + void setEnableKrb(boolean enableKrb); + + default void judgeKrbEnable() { + boolean allSet = + !Strings.isNullOrEmpty(getPrincipal()) && + !Strings.isNullOrEmpty(getKeytab()) && + !Strings.isNullOrEmpty(getKrb5conf()); + + boolean allNotSet = + Strings.isNullOrEmpty(getPrincipal()) && + Strings.isNullOrEmpty(getKeytab()) && + Strings.isNullOrEmpty(getKrb5conf()); + + if (allSet) { + setEnableKrb(true); + } else if (allNotSet) { + setEnableKrb(false); + } else { + throw new RuntimeException("Missing kerberos parameter! all kerberos params must be set, or all kerberos params are not set"); + } + } +} diff --git a/core/src/main/java/com/dtstack/flink/sql/side/BaseAsyncReqRow.java b/core/src/main/java/com/dtstack/flink/sql/side/BaseAsyncReqRow.java index ae8c75f7e..3b6a7f88c 100644 --- a/core/src/main/java/com/dtstack/flink/sql/side/BaseAsyncReqRow.java +++ b/core/src/main/java/com/dtstack/flink/sql/side/BaseAsyncReqRow.java @@ -243,12 +243,7 @@ protected ScheduledFuture registerTimer(CRow input, ResultFuture result long timeoutTimestamp = sideInfo.getSideTableInfo().getAsyncTimeout() + getProcessingTimeService().getCurrentProcessingTime(); return getProcessingTimeService().registerTimer( timeoutTimestamp, - new ProcessingTimeCallback() { - @Override - public void onProcessingTime(long timestamp) throws Exception { - timeout(input, resultFuture); - } - }); + timestamp -> timeout(input, resultFuture)); } protected void cancelTimerWhenComplete(ResultFuture resultFuture, ScheduledFuture timerFuture){ diff --git a/core/src/main/java/com/dtstack/flink/sql/util/KrbUtils.java b/core/src/main/java/com/dtstack/flink/sql/util/KrbUtils.java new file mode 100644 index 000000000..2a83359c4 --- /dev/null +++ b/core/src/main/java/com/dtstack/flink/sql/util/KrbUtils.java @@ -0,0 +1,54 @@ +/* + * 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.dtstack.flink.sql.util; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.security.UserGroupInformation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * @program: flinkStreamSQL + * @author: wuren + * @create: 2020/09/14 + **/ +public class KrbUtils { + + private static final Logger LOG = LoggerFactory.getLogger(KrbUtils.class); + + public static final String KRB5_CONF_KEY = "java.security.krb5.conf"; + public static final String HADOOP_AUTH_KEY = "hadoop.security.authentication"; + public static final String KRB_STR = "Kerberos"; +// public static final String FALSE_STR = "false"; +// public static final String SUBJECT_ONLY_KEY = "javax.security.auth.useSubjectCredsOnly"; + + public static UserGroupInformation getUgi(String principal, String keytabPath, String krb5confPath) throws IOException { + LOG.info("Kerberos login with principal: {} and keytab: {}", principal, keytabPath); + System.setProperty(KRB5_CONF_KEY, krb5confPath); + // TODO 尚未探索出此选项的意义,以后研究明白方可打开 +// System.setProperty(SUBJECT_ONLY_KEY, FALSE_STR); + Configuration configuration = new Configuration(); + configuration.set(HADOOP_AUTH_KEY , KRB_STR); + UserGroupInformation.setConfiguration(configuration); + return UserGroupInformation.loginUserFromKeytabAndReturnUGI(principal, keytabPath); + } + +} diff --git a/core/src/test/java/com/dtstack/flink/sql/util/KrbUtilsTest.java b/core/src/test/java/com/dtstack/flink/sql/util/KrbUtilsTest.java new file mode 100644 index 000000000..6c7359e14 --- /dev/null +++ b/core/src/test/java/com/dtstack/flink/sql/util/KrbUtilsTest.java @@ -0,0 +1,44 @@ +/* + * 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.dtstack.flink.sql.util; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; + +/** + * @program: flinkStreamSQL + * @author: wuren + * @create: 2020/09/14 + **/ +public class KrbUtilsTest { + @Test + public void testGetUgi() throws IOException { + String principal = ""; + String keytabPath = ""; + String krb5confPath = ""; + try { + KrbUtils.getUgi(principal, keytabPath, krb5confPath); + } catch (IllegalArgumentException e) { + Assert.assertEquals(e.getMessage(), "Can't get Kerberos realm"); + } + + } +} \ No newline at end of file diff --git a/docs/plugin/kuduSide.md b/docs/plugin/kuduSide.md index a2636d4aa..3075c3f35 100644 --- a/docs/plugin/kuduSide.md +++ b/docs/plugin/kuduSide.md @@ -60,6 +60,11 @@ | isFaultTolerant |查询是否容错 查询失败是否扫描第二个副本 默认false 容错 | 否|| | cache | 维表缓存策略(NONE/LRU/ALL)|否|NONE| | partitionedJoin | 是否在維表join之前先根据 設定的key 做一次keyby操作(可以減少维表的数据缓存量)|否|false| +| principal |kerberos用于登录的principal | 否|| +| keytab |keytab文件的路径 | 否|| +| krb5conf |conf文件路径 | 否|| +Kerberos三个参数全部设置则开启Kerberos认证,如果缺少任何一个则会提示缺少参数错误。 +如果全部未设置则不开启Kerberos连接Kudu集群。 -------------- ## 5.样例 @@ -163,3 +168,20 @@ into on t1.id = t2.id; ``` +## 7.kerberos示例 +``` +create table dim ( + name varchar, + id int, + PERIOD FOR SYSTEM_TIME +) WITH ( + type='kudu', + kuduMasters='host1', + tableName='foo', + parallelism ='1', + cache ='ALL', + keytab='foo/foobar.keytab', + krb5conf='bar/krb5.conf', + principal='kudu/host1@DTSTACK.COM' +); +``` \ No newline at end of file diff --git a/docs/plugin/kuduSink.md b/docs/plugin/kuduSink.md index cc8f8535a..7eb907770 100644 --- a/docs/plugin/kuduSink.md +++ b/docs/plugin/kuduSink.md @@ -11,7 +11,154 @@ CREATE TABLE tableName( writeMode='upsert', workerCount='1', defaultOperationTimeoutMs='600000', - defaultSocketReadTimeoutMs='6000000', + defaultSocketReadT## 1.格式: + ``` + CREATE TABLE tableName( + colName colType, + ... + colNameX colType + )WITH( + type ='kudu', + kuduMasters ='ip1,ip2,ip3', + tableName ='impala::default.test', + writeMode='upsert', + workerCount='1', + defaultOperationTimeoutMs='600000', + defaultSocketReadTimeoutMs='6000000', + parallelism ='parllNum' + ); + + + ``` + + ## 2.支持版本 + kudu 1.9.0+cdh6.2.0 + + ## 3.表结构定义 + + |参数名称|含义| + |----|---| + | tableName | 在 sql 中使用的名称;即注册到flink-table-env上的名称 + | colName | 列名称,redis中存储为 表名:主键名:主键值:列名]| + | colType | 列类型 [colType支持的类型](../colType.md)| + + + ## 4.参数: + + |参数名称|含义|是否必填|默认值| + |----|---|---|-----| + | type | 表名 输出表类型[mysq|hbase|elasticsearch|redis|kudu]|是|| + | kuduMasters | kudu master节点的地址;格式ip[ip,ip2]|是|| + | tableName | kudu 的表名称|是|| + | writeMode | 写入kudu的模式 insert|update|upsert |否 |upsert + | workerCount | 工作线程数 |否| + | defaultOperationTimeoutMs | 操作超时时间 |否| + | defaultSocketReadTimeoutMs | socket读取超时时间 |否| + | parallelism | 并行度设置|否|1| + | principal |kerberos用于登录的principal | 否|| + | keytab |keytab文件的路径 | 否|| + | krb5conf |conf文件路径 | 否|| + Kerberos三个参数全部设置则开启Kerberos认证,如果缺少任何一个则会提示缺少参数错误。 + 如果全部未设置则不开启Kerberos连接Kudu集群。 + + ## 5.样例: + ``` + CREATE TABLE MyTable( + channel varchar, + name varchar, + pv varchar, + a varchar, + b varchar + )WITH( + type ='kafka11', + bootstrapServers ='172.16.8.107:9092', + zookeeperQuorum ='172.16.8.107:2181/kafka', + offsetReset ='latest', + topic ='es_test', + timezone='Asia/Shanghai', + updateMode ='append', + enableKeyPartitions ='false', + topicIsPattern ='false', + parallelism ='1' + ); + + CREATE TABLE MyResult( + a string, + b string, + c string, + d string + )WITH( + type ='kudu', + kuduMasters ='cdh03.cdhsite:7051', + tableName ='myresult', + writeMode='insert', + parallelism ='1' + ); + + CREATE TABLE sideTable( + c string, + d string, + PRIMARY KEY(c) , + PERIOD FOR SYSTEM_TIME + )WITH( + type ='kudu', + kuduMasters ='cdh03.cdhsite:7051', + tableName ='sidetest4', + partitionedJoin ='false', + cache ='LRU', + cacheSize ='10000', + cacheTTLMs ='60000', + parallelism ='1', + primaryKey ='c', + isFaultTolerant ='false' + ); + + insert + into + MyResult + select + MyTable.a, + MyTable.b, + s.c, + s.d + from + MyTable + join + sideTable s + on MyTable.a = s.c + where + MyTable.a='2' + and s.d='2' + + ``` + + ## 6.数据示例 + ### 输入数据 + ``` + {"channel":"daishuyun","name":"roc","pv":"10","a":"2","b":"2"} + ``` + ### 结果数据 + ``` + {"a":"2","b":"2","c":"3","d":"4"} + ``` + + ## 7.kerberos示例 + ``` + create table dwd ( + name varchar, + id int + ) WITH ( + type='kudu', + kuduMasters='host1', + tableName='foo', + writeMode='insert', + parallelism ='1', + keytab='foo/foobar.keytab', + krb5conf='bar/krb5.conf', + principal='kudu/host1@DTSTACK.COM' + ); + ``` +imeoutMs='6000000', parallelism ='parllNum' ); diff --git a/impala/impala-side/impala-all-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAllReqRow.java b/impala/impala-side/impala-all-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAllReqRow.java index 10938308a..179e52e56 100644 --- a/impala/impala-side/impala-all-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAllReqRow.java +++ b/impala/impala-side/impala-all-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAllReqRow.java @@ -18,21 +18,23 @@ package com.dtstack.flink.sql.side.impala; +import com.dtstack.flink.sql.side.AbstractSideTableInfo; import com.dtstack.flink.sql.side.FieldInfo; import com.dtstack.flink.sql.side.JoinInfo; -import com.dtstack.flink.sql.side.AbstractSideTableInfo; import com.dtstack.flink.sql.side.impala.table.ImpalaSideTableInfo; import com.dtstack.flink.sql.side.rdb.all.AbstractRdbAllReqRow; import com.dtstack.flink.sql.util.JDBCUtils; +import com.dtstack.flink.sql.util.KrbUtils; import org.apache.flink.api.java.typeutils.RowTypeInfo; -import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.security.UserGroupInformation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.security.PrivilegedAction; import java.sql.Connection; import java.sql.DriverManager; +import java.sql.SQLException; import java.util.List; /** @@ -61,10 +63,26 @@ public ImpalaAllReqRow(RowTypeInfo rowTypeInfo, JoinInfo joinInfo, List) () -> { + try { + return DriverManager.getConnection(url); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + }); + } else { + connection = DriverManager.getConnection(url); + } connection.setAutoCommit(false); return connection; } catch (Exception e) { @@ -78,14 +96,11 @@ public String getUrl() throws IOException { String newUrl = ""; Integer authMech = impalaSideTableInfo.getAuthMech(); - StringBuffer urlBuffer = new StringBuffer(impalaSideTableInfo.getUrl()); + StringBuilder urlBuffer = new StringBuilder(impalaSideTableInfo.getUrl()); if (authMech == 0) { newUrl = urlBuffer.toString(); } else if (authMech == 1) { - String keyTabFilePath = impalaSideTableInfo.getKeyTabFilePath(); - String krb5FilePath = impalaSideTableInfo.getKrb5FilePath(); - String principal = impalaSideTableInfo.getPrincipal(); String krbRealm = impalaSideTableInfo.getKrbRealm(); String krbHostFQDN = impalaSideTableInfo.getKrbHostFQDN(); String krbServiceName = impalaSideTableInfo.getKrbServiceName(); @@ -96,11 +111,7 @@ public String getUrl() throws IOException { .concat("KrbServiceName=").concat(krbServiceName).concat(";") ); newUrl = urlBuffer.toString(); - System.setProperty("java.security.krb5.conf", krb5FilePath); - Configuration configuration = new Configuration(); - configuration.set("hadoop.security.authentication" , "Kerberos"); - UserGroupInformation.setConfiguration(configuration); - UserGroupInformation.loginUserFromKeytab(principal, keyTabFilePath); + } else if (authMech == 2) { String uName = impalaSideTableInfo.getUserName(); diff --git a/impala/impala-side/impala-async-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAsyncReqRow.java b/impala/impala-side/impala-async-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAsyncReqRow.java index 3f4817580..ae8ac2be9 100644 --- a/impala/impala-side/impala-async-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAsyncReqRow.java +++ b/impala/impala-side/impala-async-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAsyncReqRow.java @@ -18,27 +18,32 @@ package com.dtstack.flink.sql.side.impala; -import com.dtstack.flink.sql.factory.DTThreadFactory; +import com.dtstack.flink.sql.side.AbstractSideTableInfo; import com.dtstack.flink.sql.side.FieldInfo; import com.dtstack.flink.sql.side.JoinInfo; -import com.dtstack.flink.sql.side.AbstractSideTableInfo; import com.dtstack.flink.sql.side.impala.table.ImpalaSideTableInfo; import com.dtstack.flink.sql.side.rdb.async.RdbAsyncReqRow; +import com.dtstack.flink.sql.util.KrbUtils; import io.vertx.core.Vertx; import io.vertx.core.VertxOptions; import io.vertx.core.json.JsonObject; import io.vertx.ext.jdbc.JDBCClient; +import io.vertx.ext.sql.SQLClient; import org.apache.flink.api.java.typeutils.RowTypeInfo; import org.apache.flink.configuration.Configuration; +import org.apache.flink.streaming.api.functions.async.ResultFuture; +import org.apache.flink.table.runtime.types.CRow; +import org.apache.flink.types.Row; import org.apache.hadoop.security.UserGroupInformation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; +import java.security.PrivilegedAction; import java.util.List; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; /** * Date: 2019/11/12 @@ -53,6 +58,7 @@ public class ImpalaAsyncReqRow extends RdbAsyncReqRow { private final static String IMPALA_DRIVER = "com.cloudera.impala.jdbc41.Driver"; + protected UserGroupInformation ugi = null; public ImpalaAsyncReqRow(RowTypeInfo rowTypeInfo, JoinInfo joinInfo, List outFieldInfoList, AbstractSideTableInfo sideTableInfo) { super(new ImpalaAsyncSideInfo(rowTypeInfo, joinInfo, outFieldInfoList, sideTableInfo)); @@ -60,9 +66,19 @@ public ImpalaAsyncReqRow(RowTypeInfo rowTypeInfo, JoinInfo joinInfo, List inputParams, + CRow input, + ResultFuture resultFuture, + SQLClient rdbSqlClient, + AtomicLong failCounter, + AtomicBoolean finishFlag, + CountDownLatch latch) { + if (ugi == null) { + doAsyncQueryData(inputParams, + input, resultFuture, + rdbSqlClient, + failCounter, + finishFlag, + latch); + } else { + // Kerberos + ugi.doAs((PrivilegedAction) () -> { + doAsyncQueryData(inputParams, + input, resultFuture, + rdbSqlClient, + failCounter, + finishFlag, + latch); + return null; + }); + } + } } diff --git a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java new file mode 100644 index 000000000..19e904ef2 --- /dev/null +++ b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java @@ -0,0 +1,754 @@ +/* + * 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.dtstack.flink.sql.sink.impala; + +import com.dtstack.flink.sql.factory.DTThreadFactory; +import com.dtstack.flink.sql.outputformat.AbstractDtRichOutputFormat; +import com.dtstack.flink.sql.sink.rdb.JDBCTypeConvertUtils; +import com.dtstack.flink.sql.table.AbstractTableInfo; +import com.dtstack.flink.sql.util.JDBCUtils; +import com.dtstack.flink.sql.util.KrbUtils; +import com.google.common.collect.Maps; +import org.apache.commons.collections.CollectionUtils; +import org.apache.flink.api.java.tuple.Tuple2; +import org.apache.flink.configuration.Configuration; +import org.apache.flink.types.Row; +import org.apache.hadoop.security.UserGroupInformation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.rmi.RemoteException; +import java.security.PrivilegedExceptionAction; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static com.dtstack.flink.sql.sink.rdb.JDBCTypeConvertUtils.setRecordToStatement; +import static org.apache.flink.util.Preconditions.checkNotNull; + +/** + * Date: 2020/10/14 + * Company: www.dtstack.com + * + * @author tiezhu + */ +public class ImpalaOutputFormat extends AbstractDtRichOutputFormat> { + + private static final Logger LOG = LoggerFactory.getLogger(ImpalaOutputFormat.class); + + private static final long serialVersionUID = 1L; + + // ${field} + private static final Pattern STATIC_PARTITION_PATTERN = Pattern.compile("\\$\\{([^}]*)}"); + // cast(value as string) -> cast('value' as string) cast(value as timestamp) -> cast('value' as timestamp) + private static final Pattern TYPE_PATTERN = Pattern.compile("cast\\((.*) as (.*)\\)"); + //specific type which values need to be quoted + private static final String[] NEED_QUOTE_TYPE = {"string", "timestamp"}; + + private static final Integer DEFAULT_CONN_TIME_OUT = 60; + private static final int RECEIVE_DATA_PRINT_FREQUENCY = 1000; + private static final int DIRTY_DATA_PRINT_FREQUENCY = 1000; + + private static final String KUDU_TYPE = "kudu"; + private static final String UPDATE_MODE = "update"; + private static final String PARTITION_CONSTANT = "PARTITION"; + private static final String DRIVER_NAME = "com.cloudera.impala.jdbc41.Driver"; + + private static final String VALUES_CONDITION = "${valuesCondition}"; + private static final String PARTITION_CONDITION = "${partitionCondition}"; + private static final String TABLE_FIELDS_CONDITION = "${tableFieldsCondition}"; + private static final String NO_PARTITION = "noPartition"; + + protected transient Connection connection; + protected transient Statement statement; + protected transient PreparedStatement updateStatement; + + private transient volatile boolean closed = false; + private int batchCount = 0; + + // |------------------------------------------------| + // | partitionCondition |Array of valueCondition| + // |------------------------------------------------| + // | ptOne, ptTwo, ptThree | [(v1, v2, v3, v4, v5)]| DP + // |------------------------------------------------| + // | ptOne = v1, ptTwo = v2 | [(v3, v4, v5)] | SP + // |------------------------------------------------| + // | ptOne, ptTwo = v2 | [(v1, v3, v4, v5)] | DP and SP + // |------------------------------------------------| + // | noPartition | [(v1, v2, v3, v4, v5)]| kudu or disablePartition + // |------------------------------------------------| + private transient Map> rowDataMap; + + protected String keytabPath; + protected String krb5confPath; + protected String principal; + protected Integer authMech; + protected String dbUrl; + protected String userName; + protected String password; + protected int batchSize = 100; + protected long batchWaitInterval = 60 * 1000L; + protected String tableName; + protected List primaryKeys; + protected String partitionFields; + protected Boolean enablePartition; + protected String schema; + protected String storeType; + protected String updateMode; + public List fieldNames; + public List fieldTypes; + public List fieldExtraInfoList; + + // partition field of static partition which matched by ${field} + private final List staticPartitionFields = new ArrayList<>(); + + // valueFieldsName -> 重组之后的fieldNames,为了重组row data字段值对应 + // 需要对partition字段做特殊处理,比如原来的字段顺序为(age, name, id),但是因为partition,写入的SQL为 + // INSERT INTO tableName(name, id) PARTITION(age) VALUES(?, ?, ?) + // 那么实际executeSql设置字段的顺序应该为(name, id, age),同时,字段对应的type顺序也需要重组 + private List valueFieldNames; + private transient AbstractDtRichOutputFormat metricOutputFormat; + private List rows; + + private transient ScheduledExecutorService scheduler; + private transient ScheduledFuture scheduledFuture; + + @Override + public void configure(Configuration parameters) { + } + + @Override + public void open(int taskNumber, int numTasks) throws IOException { + try { + rowDataMap = new HashMap<>(); + rows = new ArrayList<>(); + metricOutputFormat = this; + openConnect(); + initScheduledTask(batchWaitInterval); + init(); + initMetric(); + } catch (Exception e) { + throw new RemoteException("impala output format open error!", e); + } + } + + private void init() throws SQLException { + if (Objects.nonNull(partitionFields)) { + // match ${field} from partitionFields + Matcher matcher = STATIC_PARTITION_PATTERN.matcher(partitionFields); + while (matcher.find()) { + LOG.info("find static partition field: {}", matcher.group(1)); + staticPartitionFields.add(matcher.group(1)); + } + } + + if (updateMode.equalsIgnoreCase(UPDATE_MODE)) { + if (!storeType.equalsIgnoreCase(KUDU_TYPE)) { + throw new IllegalArgumentException("update mode not support for non-kudu table!"); + } + + updateStatement = connection.prepareStatement(buildUpdateSql(schema, tableName, fieldNames, primaryKeys)); + return; + } + + valueFieldNames = rebuildFieldNameListAndTypeList(fieldNames, staticPartitionFields, fieldTypes, partitionFields); + } + + private void initScheduledTask(Long batchWaitInterval) { + if (batchWaitInterval != 0) { + this.scheduler = new ScheduledThreadPoolExecutor(1, + new DTThreadFactory("impala-upsert-output-format")); + this.scheduledFuture = this.scheduler.scheduleWithFixedDelay(() -> { + synchronized (ImpalaOutputFormat.this) { + try { + flush(); + } catch (Exception e) { + LOG.error("Writing records to impala jdbc failed.", e); + throw new RuntimeException("Writing records to impala jdbc failed.", e); + } + } + }, batchWaitInterval, batchWaitInterval, TimeUnit.MILLISECONDS); + } + } + + private void openConnect() throws IOException { + if (authMech == 1) { + UserGroupInformation ugi = KrbUtils.getUgi(principal, keytabPath, krb5confPath); + try { + ugi.doAs((PrivilegedExceptionAction) () -> { + openJdbc(); + return null; + }); + } catch (InterruptedException | IOException e) { + throw new IllegalArgumentException("connect impala error!", e); + } + } else { + openJdbc(); + } + } + + /** + * get jdbc connection + */ + private void openJdbc() { + JDBCUtils.forName(DRIVER_NAME, getClass().getClassLoader()); + try { + connection = DriverManager.getConnection(dbUrl, userName, password); + statement = connection.createStatement(); + connection.setAutoCommit(false); + } catch (SQLException sqlException) { + throw new RuntimeException("get impala jdbc connection failed!", sqlException); + } + } + + private synchronized void flush() throws SQLException { + if (batchCount > 0) { + if (updateMode.equalsIgnoreCase(UPDATE_MODE)) { + executeUpdateBatch(); + } + if (!rowDataMap.isEmpty()) { + String templateSql = + "INSERT INTO tableName ${tableFieldsCondition} PARTITION ${partitionCondition} VALUES ${valuesCondition}"; + executeBatchSql( + statement, + templateSql, + schema, + tableName, + storeType, + enablePartition, + valueFieldNames, + partitionFields, + rowDataMap + ); + rowDataMap.clear(); + } + } + batchCount = 0; + + } + + /** + * execute batch update statement + * + * @throws SQLException throw sql exception + */ + private void executeUpdateBatch() throws SQLException { + try { + rows.forEach(row -> { + try { + JDBCTypeConvertUtils.setRecordToStatement( + updateStatement, + JDBCTypeConvertUtils.getSqlTypeFromFieldType(fieldTypes), + row, + primaryKeys.stream().mapToInt(fieldNames::indexOf).toArray() + ); + updateStatement.addBatch(); + } catch (Exception e) { + throw new RuntimeException("impala jdbc execute batch error!", e); + } + }); + updateStatement.executeBatch(); + connection.commit(); + rows.clear(); + } catch (Exception e) { + LOG.debug("impala jdbc execute batch error ", e); + connection.rollback(); + connection.commit(); + updateStatement.clearBatch(); + executeUpdate(connection); + } + } + + public void executeUpdate(Connection connection) { + rows.forEach(row -> { + try { + setRecordToStatement(updateStatement, JDBCTypeConvertUtils.getSqlTypeFromFieldType(fieldTypes), row); + updateStatement.executeUpdate(); + connection.commit(); + } catch (Exception e) { + try { + connection.rollback(); + connection.commit(); + } catch (SQLException e1) { + throw new RuntimeException(e1); + } + if (metricOutputFormat.outDirtyRecords.getCount() % DIRTY_DATA_PRINT_FREQUENCY == 0 || LOG.isDebugEnabled()) { + LOG.error("record insert failed ,this row is {}", row.toString()); + LOG.error("", e); + } + metricOutputFormat.outDirtyRecords.inc(); + } + }); + rows.clear(); + } + + private void putRowIntoMap(Map> rowDataMap, Tuple2 rowData) { + Set keySet = rowDataMap.keySet(); + ArrayList tempRowArray; + if (keySet.contains(rowData.f0)) { + tempRowArray = rowDataMap.get(rowData.f0); + } else { + tempRowArray = new ArrayList<>(); + } + tempRowArray.add(rowData.f1); + rowDataMap.put(rowData.f0, tempRowArray); + } + + private List rebuildFieldNameListAndTypeList(List fieldNames, + List staticPartitionFields, + List fieldTypes, + String partitionFields) { + if (partitionFields.isEmpty()) { + return fieldNames; + } + + List valueFields = new ArrayList<>(fieldNames); + + for (int i = valueFields.size() - 1; i >= 0; i--) { + if (staticPartitionFields.contains(fieldNames.get(i))) { + valueFields.remove(i); + fieldTypes.remove(i); + } + } + + for (int i = 0; i < valueFields.size(); i++) { + if (partitionFields.contains(fieldNames.get(i))) { + valueFields.add(valueFields.remove(i)); + fieldTypes.add(fieldTypes.remove(i)); + } + } + + return valueFields; + } + + /** + * Quote a specific type of value, like string, timestamp + * before: 1, cast(tiezhu as string), cast(2001-01-09 01:05:01 as timestamp), cast(123 as int) + * after: 1, cast('tiezhu' as string), cast('2001-01-09 01:05:01' as timestamp), cast(123 as int) + * if cast value is null, then cast(null as type) + * + * @param valueCondition original value condition + * @return quoted condition + */ + private String valueConditionAddQuotation(String valueCondition) { + final String[] valueConditionCopy = {valueCondition}; + String[] temps = valueCondition.split(","); + Arrays.stream(temps).forEach( + item -> { + Matcher matcher = TYPE_PATTERN.matcher(item); + while (matcher.find()) { + String value = matcher.group(1); + String type = matcher.group(2); + + if (Arrays.asList(NEED_QUOTE_TYPE).contains(type)) { + if (!"null".equals(value)) { + valueConditionCopy[0] = valueConditionCopy[0].replace(value, "'" + value + "'"); + } + } + } + } + ); + return "(" + valueConditionCopy[0] + ")"; + } + + @Override + public synchronized void writeRecord(Tuple2 record) throws IOException { + try { + if (!record.f0) { + return; + } + + if (outRecords.getCount() % RECEIVE_DATA_PRINT_FREQUENCY == 0 || LOG.isDebugEnabled()) { + LOG.info("Receive data : {}", record); + } + + if (updateMode.equalsIgnoreCase(UPDATE_MODE)) { + rows.add(Row.copy(record.f1)); + } else { + Map valueMap = Maps.newHashMap(); + Row row = Row.copy(record.f1); + + for (int i = 0; i < row.getArity(); i++) { + valueMap.put(fieldNames.get(i), row.getField(i)); + } + + Tuple2 rowTuple2 = new Tuple2<>(); + if (storeType.equalsIgnoreCase(KUDU_TYPE) || !enablePartition) { + rowTuple2.f0 = NO_PARTITION; + } else { + rowTuple2.f0 = buildPartitionCondition(valueMap, partitionFields, staticPartitionFields); + } + + // 根据字段名对 row data 重组, 比如,原始 row data : (1, xxx, 20) -> (id, name, age) + // 但是由于 partition,写入的field 顺序变成了 (name, id, age),则需要对 row data 重组变成 (xxx, 1, 20) + Row rowValue = new Row(fieldTypes.size()); + for (int i = 0; i < fieldTypes.size(); i++) { + rowValue.setField(i, valueMap.get(valueFieldNames.get(i))); + } + rowTuple2.f1 = valueConditionAddQuotation(buildValuesCondition(fieldTypes, rowValue)); + putRowIntoMap(rowDataMap, rowTuple2); + } + + batchCount++; + + if (batchCount >= batchSize) { + flush(); + } + + // Receive data + outRecords.inc(); + } catch (Exception e) { + throw new RuntimeException("Writing records to impala failed.", e); + } + } + + @Override + public void close() throws IOException { + if (closed) { + return; + } + // 将还未执行的SQL flush + if (batchCount > 0) { + try { + flush(); + } catch (Exception e) { + throw new RuntimeException("Writing records to impala failed.", e); + } + } + // cancel scheduled task + if (this.scheduledFuture != null) { + scheduledFuture.cancel(false); + this.scheduler.shutdown(); + } + // close connection + try { + if (connection != null && connection.isValid(DEFAULT_CONN_TIME_OUT)) { + connection.close(); + } + + if (statement != null && !statement.isClosed()) { + statement.close(); + } + + if (updateStatement != null && !updateStatement.isClosed()) { + updateStatement.close(); + } + } catch (SQLException e) { + throw new RemoteException("impala connection close failed!"); + } finally { + connection = null; + statement = null; + updateStatement = null; + } + closed = true; + } + + /** + * execute batch sql from row data map + * sql like 'insert into tableName(f1, f2, f3) ${partitionCondition} values(v1, v2, v3), (v4, v5, v6).... + * + * @param statement execute statement + * @param tempSql template sql + * @param storeType the store type of data + * @param enablePartition enable partition or not + * @param fieldNames field name list + * @param partitionFields partition fields + * @param rowDataMap row data map + * @throws SQLException throw sql exception + */ + private synchronized void executeBatchSql(Statement statement, + String tempSql, + String schema, + String tableName, + String storeType, + Boolean enablePartition, + List fieldNames, + String partitionFields, + Map> rowDataMap) throws SQLException { + StringBuilder valuesCondition = new StringBuilder(); + StringBuilder partitionCondition = new StringBuilder(); + String tableFieldsCondition = buildTableFieldsCondition(fieldNames, partitionFields); + ArrayList rowData; + String tableNameInfo = Objects.isNull(schema) ? + tableName : quoteIdentifier(schema) + "." + tableName; + tempSql = tempSql.replace("tableName", tableNameInfo); + + // kudu ${partitionCondition} is null + if (storeType.equalsIgnoreCase(KUDU_TYPE) || !enablePartition) { + rowData = rowDataMap.get(NO_PARTITION); + rowData.forEach(row -> valuesCondition.append(row).append(", ")); + String executeSql = tempSql.replace(VALUES_CONDITION, valuesCondition.toString()) + .replace(PARTITION_CONDITION, partitionCondition.toString()) + .replace(PARTITION_CONSTANT, "") + .replace(TABLE_FIELDS_CONDITION, tableFieldsCondition); + String substring = executeSql.substring(0, executeSql.length() - 2); + statement.execute(substring); + return; + } + + // partition sql + Set keySet = rowDataMap.keySet(); + String finalTempSql = tempSql; + keySet.forEach(key -> { + try { + String executeSql = String.copyValueOf(finalTempSql.toCharArray()); + ArrayList valuesConditionList = rowDataMap.get(key); + partitionCondition.append(key); + executeSql = executeSql.replace(PARTITION_CONDITION, partitionCondition.toString()) + .replace(TABLE_FIELDS_CONDITION, tableFieldsCondition) + .replace(VALUES_CONDITION, String.join(", ", valuesConditionList)); + statement.execute(executeSql); + partitionCondition.delete(0, partitionCondition.length()); + } catch (SQLException sqlException) { + throw new RuntimeException("execute impala partition SQL error! ", sqlException); + } + }); + } + + /** + * build partition condition with row data + * + * @param rowData row data + * @param partitionFields partition fields + * @param staticPartitionField static partition fields + * @return condition like '(ptOne, ptTwo=v2)' + */ + private String buildPartitionCondition(Map rowData, String partitionFields, List staticPartitionField) { + for (String key : staticPartitionField) { + StringBuilder sb = new StringBuilder(); + Object value = rowData.get(key); + sb.append(key).append("=").append(value); + partitionFields = partitionFields.replace("${" + key + "}", sb.toString()); + } + return "(" + partitionFields + ")"; + } + + /** + * build field condition according to field names + * replace ${tableFieldCondition} + * + * @param fieldNames the selected field names + * @param partitionFields the partition fields + * @return condition like '(id, name, age)' + */ + private String buildTableFieldsCondition(List fieldNames, String partitionFields) { + return "(" + fieldNames.stream() + .filter(f -> !partitionFields.contains(f)) + .map(this::quoteIdentifier) + .collect(Collectors.joining(", ")) + ")"; + } + + /** + * according to field types, build the values condition + * replace ${valuesCondition} + * + * @param fieldTypes field types + * @return condition like '(?, ?, cast(? as string))' and '?' will be replaced with row data + */ + private String buildValuesCondition(List fieldTypes, Row row) { + String valuesCondition = fieldTypes.stream().map( + f -> { + if (Arrays.asList(NEED_QUOTE_TYPE).contains(f.toLowerCase())) { + return String.format("cast(? as %s)", f.toLowerCase()); + } + return "?"; + }).collect(Collectors.joining(", ")); + for (int i = 0; i < row.getArity(); i++) { + valuesCondition = valuesCondition.replaceFirst("\\?", Objects.isNull(row.getField(i)) ? "null" : row.getField(i).toString()); + } + return valuesCondition; + } + + /** + * impala update mode SQL + * + * @return UPDATE tableName SET setCondition WHERE whereCondition + */ + private String buildUpdateSql(String schema, String tableName, List fieldNames, List primaryKeys) { + //跳过primary key字段 + String setClause = fieldNames.stream() + .filter(f -> !CollectionUtils.isNotEmpty(primaryKeys) || !primaryKeys.contains(f)) + .map(f -> quoteIdentifier(f) + "=?") + .collect(Collectors.joining(", ")); + + String conditionClause = primaryKeys.stream() + .map(f -> quoteIdentifier(f) + "=?") + .collect(Collectors.joining(" AND ")); + + return "UPDATE " + (Objects.isNull(schema) ? "" : quoteIdentifier(schema) + ".") + + quoteIdentifier(tableName) + " SET " + setClause + " WHERE " + conditionClause; + } + + private String quoteIdentifier(String identifier) { + return "`" + identifier + "`"; + } + + public static Builder getImpalaBuilder() { + return new Builder(); + } + + public static class Builder { + private final ImpalaOutputFormat format = new ImpalaOutputFormat(); + + public Builder setDbUrl(String dbUrl) { + format.dbUrl = dbUrl; + return this; + } + + public Builder setUserName(String userName) { + format.userName = userName; + return this; + } + + public Builder setPassword(String password) { + format.password = password; + return this; + } + + public Builder setBatchSize(Integer batchSize) { + format.batchSize = batchSize; + return this; + } + + public Builder setBatchWaitInterval(Long batchWaitInterval) { + format.batchWaitInterval = batchWaitInterval; + return this; + } + + public Builder setTableName(String tableName) { + format.tableName = tableName; + return this; + } + + public Builder setPartitionFields(String partitionFields) { + format.partitionFields = Objects.isNull(partitionFields) ? + "" : partitionFields; + return this; + } + + public Builder setPrimaryKeys(List primaryKeys) { + format.primaryKeys = primaryKeys; + return this; + } + + public Builder setSchema(String schema) { + format.schema = schema; + return this; + } + + public Builder setEnablePartition(Boolean enablePartition) { + format.enablePartition = enablePartition; + return this; + } + + public Builder setUpdateMode(String updateMode) { + format.updateMode = updateMode; + return this; + } + + public Builder setFieldList(List fieldList) { + format.fieldNames = fieldList; + return this; + } + + public Builder setFieldTypeList(List fieldTypeList) { + format.fieldTypes = fieldTypeList; + return this; + } + + public Builder setStoreType(String storeType) { + format.storeType = storeType; + return this; + } + + public Builder setFieldExtraInfoList(List fieldExtraInfoList) { + format.fieldExtraInfoList = fieldExtraInfoList; + return this; + } + + public Builder setKeyTabPath(String keyTabPath) { + format.keytabPath = keyTabPath; + return this; + } + + public Builder setKrb5ConfPath(String krb5ConfPath) { + format.krb5confPath = krb5ConfPath; + return this; + } + + public Builder setPrincipal(String principal) { + format.principal = principal; + return this; + } + + public Builder setAuthMech(Integer authMech) { + format.authMech = authMech; + return this; + } + + private boolean canHandle(String url) { + return url.startsWith("jdbc:impala:"); + } + + public ImpalaOutputFormat build() { + if (!canHandle(format.dbUrl)) { + throw new IllegalArgumentException("impala dbUrl is illegal, check url: " + format.dbUrl); + } + + if (format.authMech == EAuthMech.Kerberos.getType()) { + checkNotNull(format.krb5confPath, + "When kerberos authentication is enabled, krb5confPath is required!"); + checkNotNull(format.principal, + "When kerberos authentication is enabled, principal is required!"); + checkNotNull(format.keytabPath, + "When kerberos authentication is enabled, keytabPath is required!"); + } + + if (format.authMech == EAuthMech.UserName.getType()) { + checkNotNull(format.userName, "userName is required!"); + } + + if (format.authMech == EAuthMech.NameANDPassword.getType()) { + checkNotNull(format.userName, "userName is required!"); + checkNotNull(format.password, "password is required!"); + } + + return format; + } + + } + +} diff --git a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaSink.java b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaSink.java index 34101d410..6d28f0173 100644 --- a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaSink.java +++ b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaSink.java @@ -20,64 +20,115 @@ import com.dtstack.flink.sql.sink.IStreamSinkGener; import com.dtstack.flink.sql.sink.impala.table.ImpalaTableInfo; -import com.dtstack.flink.sql.sink.rdb.JDBCOptions; -import com.dtstack.flink.sql.sink.rdb.AbstractRdbSink; -import com.dtstack.flink.sql.sink.rdb.format.JDBCUpsertOutputFormat; +import com.dtstack.flink.sql.table.AbstractTableInfo; import com.dtstack.flink.sql.table.AbstractTargetTableInfo; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.security.UserGroupInformation; +import org.apache.flink.api.common.typeinfo.TypeInformation; +import org.apache.flink.api.java.tuple.Tuple2; +import org.apache.flink.api.java.typeutils.RowTypeInfo; +import org.apache.flink.api.java.typeutils.TupleTypeInfo; +import org.apache.flink.streaming.api.datastream.DataStream; +import org.apache.flink.streaming.api.datastream.DataStreamSink; +import org.apache.flink.streaming.api.functions.sink.OutputFormatSinkFunction; +import org.apache.flink.streaming.api.functions.sink.RichSinkFunction; +import org.apache.flink.table.sinks.RetractStreamTableSink; +import org.apache.flink.table.sinks.TableSink; +import org.apache.flink.types.Row; -import java.io.IOException; +import java.util.List; +import java.util.Objects; /** - * Date: 2019/11/11 + * Date: 2020/10/14 * Company: www.dtstack.com * - * @author xiuzhu + * @author tiezhu */ +public class ImpalaSink implements RetractStreamTableSink, IStreamSinkGener { -public class ImpalaSink extends AbstractRdbSink implements IStreamSinkGener { + private static final String DEFAULT_STORE_TYPE = "kudu"; - private ImpalaTableInfo impalaTableInfo; + protected String[] fieldNames; + TypeInformation[] fieldTypes; + protected String dbUrl; + protected String userName; + protected String password; + protected Integer authMech; + + protected String keytabPath; + protected String krb5confPath; + protected String principal; + + protected int batchSize = 100; + protected long batchWaitInterval = 60 * 1000L; + protected String tableName; + protected String registerTabName; + protected String storeType; + + protected List primaryKeys; + private int parallelism = 1; + protected String schema; + protected String updateMode; + protected Boolean enablePartition; + public List fieldList; + public List fieldTypeList; + public List fieldExtraInfoList; + protected String partitionFields; public ImpalaSink() { - super(null); + // do Nothing } @Override - public JDBCUpsertOutputFormat getOutputFormat() { - JDBCOptions jdbcOptions = JDBCOptions.builder() - .setDbUrl(getImpalaJdbcUrl()) - .setDialect(new ImpalaDialect(getFieldTypes(), primaryKeys)) - .setUsername(userName) - .setPassword(password) - .setTableName(tableName) - .build(); + public ImpalaSink genStreamSink(AbstractTargetTableInfo targetTableInfo) { + ImpalaTableInfo impalaTableInfo = (ImpalaTableInfo) targetTableInfo; + this.dbUrl = getImpalaJdbcUrl(impalaTableInfo); + this.password = impalaTableInfo.getPassword(); + this.userName = impalaTableInfo.getUserName(); + this.authMech = impalaTableInfo.getAuthMech(); - return JDBCUpsertOutputFormat.builder() - .setOptions(jdbcOptions) - .setFieldNames(fieldNames) - .setFlushMaxSize(batchNum) - .setFlushIntervalMills(batchWaitInterval) - .setFieldTypes(sqlTypes) - .setKeyFields(primaryKeys) - .setPartitionFields(impalaTableInfo.getPartitionFields()) - .setAllReplace(allReplace) - .setUpdateMode(updateMode) - .build(); - } + this.principal = impalaTableInfo.getPrincipal(); + this.keytabPath = impalaTableInfo.getKeyTabFilePath(); + this.krb5confPath = impalaTableInfo.getKrb5FilePath(); + + this.updateMode = Objects.isNull(impalaTableInfo.getUpdateMode()) ? + "append" : impalaTableInfo.getUpdateMode(); + + this.batchSize = Objects.isNull(impalaTableInfo.getBatchSize()) ? + batchSize : impalaTableInfo.getBatchSize(); + this.batchWaitInterval = Objects.isNull(impalaTableInfo.getBatchWaitInterval()) ? + batchWaitInterval : impalaTableInfo.getBatchWaitInterval(); + this.parallelism = Objects.isNull(impalaTableInfo.getParallelism()) ? + parallelism : impalaTableInfo.getParallelism(); + this.registerTabName = impalaTableInfo.getTableName(); + this.fieldList = impalaTableInfo.getFieldList(); + this.fieldTypeList = impalaTableInfo.getFieldTypeList(); + this.fieldExtraInfoList = impalaTableInfo.getFieldExtraInfoList(); + this.tableName = impalaTableInfo.getTableName(); + this.schema = impalaTableInfo.getSchema(); + this.primaryKeys = impalaTableInfo.getPrimaryKeys(); + this.partitionFields = impalaTableInfo.getPartitionFields(); - public String getImpalaJdbcUrl() { + this.storeType = Objects.isNull(impalaTableInfo.getStoreType()) ? + DEFAULT_STORE_TYPE : impalaTableInfo.getStoreType(); + this.enablePartition = impalaTableInfo.isEnablePartition(); + + return this; + } + + /** + * build Impala Jdbc Url according to authMech + * + * @param impalaTableInfo impala table info + * @return jdbc url with auth mech info + */ + public String getImpalaJdbcUrl(ImpalaTableInfo impalaTableInfo) { Integer authMech = impalaTableInfo.getAuthMech(); - String newUrl = dbUrl; - StringBuffer urlBuffer = new StringBuffer(dbUrl); + String newUrl = impalaTableInfo.getUrl(); + StringBuilder urlBuffer = new StringBuilder(impalaTableInfo.getUrl()); if (authMech == EAuthMech.NoAuthentication.getType()) { return newUrl; } else if (authMech == EAuthMech.Kerberos.getType()) { - String keyTabFilePath = impalaTableInfo.getKeyTabFilePath(); - String krb5FilePath = impalaTableInfo.getKrb5FilePath(); - String principal = impalaTableInfo.getPrincipal(); String krbRealm = impalaTableInfo.getKrbRealm(); String krbHostFqdn = impalaTableInfo.getKrbHostFQDN(); String krbServiceName = impalaTableInfo.getKrbServiceName(); @@ -88,21 +139,10 @@ public String getImpalaJdbcUrl() { .concat("KrbServiceName=").concat(krbServiceName).concat(";") ); newUrl = urlBuffer.toString(); - - System.setProperty("java.security.krb5.conf", krb5FilePath); - Configuration configuration = new Configuration(); - configuration.set("hadoop.security.authentication", "Kerberos"); - UserGroupInformation.setConfiguration(configuration); - try { - UserGroupInformation.loginUserFromKeytab(principal, keyTabFilePath); - } catch (IOException e) { - throw new RuntimeException("loginUserFromKeytab error ..", e); - } - } else if (authMech == EAuthMech.UserName.getType()) { urlBuffer.append(";" .concat("AuthMech=3;") - .concat("UID=").concat(userName).concat(";") + .concat("UID=").concat(impalaTableInfo.getUserName()).concat(";") .concat("PWD=;") .concat("UseSasl=0") ); @@ -110,8 +150,8 @@ public String getImpalaJdbcUrl() { } else if (authMech == EAuthMech.NameANDPassword.getType()) { urlBuffer.append(";" .concat("AuthMech=3;") - .concat("UID=").concat(userName).concat(";") - .concat("PWD=").concat(password) + .concat("UID=").concat(impalaTableInfo.getUserName()).concat(";") + .concat("PWD=").concat(impalaTableInfo.getPassword()) ); newUrl = urlBuffer.toString(); } else { @@ -120,11 +160,69 @@ public String getImpalaJdbcUrl() { return newUrl; } + private ImpalaOutputFormat buildImpalaOutputFormat() { + + return ImpalaOutputFormat.getImpalaBuilder() + .setDbUrl(dbUrl) + .setPassword(password) + .setUserName(userName) + .setSchema(schema) + .setTableName(tableName) + .setUpdateMode(updateMode) + .setBatchSize(batchSize) + .setBatchWaitInterval(batchWaitInterval) + .setPrimaryKeys(primaryKeys) + .setPartitionFields(partitionFields) + .setFieldList(fieldList) + .setFieldTypeList(fieldTypeList) + .setFieldExtraInfoList(fieldExtraInfoList) + .setStoreType(storeType) + .setEnablePartition(enablePartition) + .setUpdateMode(updateMode) + .setAuthMech(authMech) + .setKeyTabPath(keytabPath) + .setKrb5ConfPath(krb5confPath) + .setPrincipal(principal) + .build(); + } + + @Override + public void emitDataStream(DataStream> dataStream) { + consumeDataStream(dataStream); + } + + public void consumeDataStream(DataStream> dataStream) { + ImpalaOutputFormat outputFormat = buildImpalaOutputFormat(); + RichSinkFunction> richSinkFunction = new OutputFormatSinkFunction(outputFormat); + dataStream.addSink(richSinkFunction) + .setParallelism(parallelism) + .name(tableName); + } + @Override - public AbstractRdbSink genStreamSink(AbstractTargetTableInfo targetTableInfo) { - super.genStreamSink(targetTableInfo); - this.impalaTableInfo = (ImpalaTableInfo) targetTableInfo; + public TableSink> configure(String[] fieldNames, TypeInformation[] fieldTypes) { + this.fieldNames = fieldNames; + this.fieldTypes = fieldTypes; return this; } + @Override + public TupleTypeInfo> getOutputType() { + return new TupleTypeInfo<>(org.apache.flink.table.api.Types.BOOLEAN(), getRecordType()); + } + + @Override + public TypeInformation getRecordType() { + return new RowTypeInfo(fieldTypes, fieldNames); + } + + @Override + public String[] getFieldNames() { + return fieldNames; + } + + @Override + public TypeInformation[] getFieldTypes() { + return fieldTypes; + } } diff --git a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/table/ImpalaSinkParser.java b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/table/ImpalaSinkParser.java index 0d4857809..f1a475f61 100644 --- a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/table/ImpalaSinkParser.java +++ b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/table/ImpalaSinkParser.java @@ -18,109 +18,125 @@ package com.dtstack.flink.sql.sink.impala.table; -import com.dtstack.flink.sql.sink.rdb.table.RdbSinkParser; import com.dtstack.flink.sql.table.AbstractTableInfo; +import com.dtstack.flink.sql.table.AbstractTableParser; import com.dtstack.flink.sql.util.MathUtil; -import java.math.BigDecimal; -import java.sql.Timestamp; import java.util.Arrays; import java.util.List; import java.util.Map; /** - * Reason: - * Date: 2019/11/11 + * Date: 2020/10/14 * Company: www.dtstack.com * - * @author xiuzhu + * @author tiezhu */ +public class ImpalaSinkParser extends AbstractTableParser { -public class ImpalaSinkParser extends RdbSinkParser { + private static final String PARALLELISM_KEY = "parallelism"; + + private static final String AUTH_MECH_KEY = "authMech"; + + private static final String KRB5FILEPATH_KEY = "krb5FilePath"; + + private static final String PRINCIPAL_KEY = "principal"; + + private static final String KEY_TAB_FILE_PATH_KEY = "keyTabFilePath"; + + private static final String KRB_REALM_KEY = "krbRealm"; + + private static final String KRB_HOST_FQDN_KEY = "krbHostFQDN"; + + private static final String KRB_SERVICE_NAME_KEY = "krbServiceName"; + + private static final String ENABLE_PARTITION_KEY = "enablePartition"; + + private static final String PARTITION_FIELDS_KEY = "partitionFields"; + + private static final String URL_KEY = "url"; + + private static final String TABLE_NAME_KEY = "tableName"; + + private static final String USER_NAME_KEY = "userName"; + + private static final String PASSWORD_KEY = "password"; + + private static final String BATCH_SIZE_KEY = "batchSize"; + + private static final String BATCH_WAIT_INTERVAL_KEY = "batchWaitInterval"; + + private static final String BUFFER_SIZE_KEY = "bufferSize"; + + private static final String FLUSH_INTERVAL_MS_KEY = "flushIntervalMs"; + + private static final String SCHEMA_KEY = "schema"; + + private static final String UPDATE_KEY = "updateMode"; + + private static final String KUDU_TYPE = "kudu"; + + private static final String STORE_TYPE_KEY = "storeType"; + + private static final String KRB_DEFAULT_REALM = "HADOOP.COM"; + + private static final String CURRENT_TYPE = "impala"; - private static final String CURR_TYPE = "impala"; @Override - public AbstractTableInfo getTableInfo(String tableName, String fieldsInfo, Map props) { + public AbstractTableInfo getTableInfo(String tableName, String fieldsInfo, Map props) throws Exception { ImpalaTableInfo impalaTableInfo = new ImpalaTableInfo(); impalaTableInfo.setName(tableName); parseFieldsInfo(fieldsInfo, impalaTableInfo); - impalaTableInfo.setParallelism(MathUtil.getIntegerVal(props.get(ImpalaTableInfo.PARALLELISM_KEY.toLowerCase()))); - impalaTableInfo.setUrl(MathUtil.getString(props.get(ImpalaTableInfo.URL_KEY.toLowerCase()))); - impalaTableInfo.setTableName(MathUtil.getString(props.get(ImpalaTableInfo.TABLE_NAME_KEY.toLowerCase()))); - impalaTableInfo.setBatchSize(MathUtil.getIntegerVal(props.get(ImpalaTableInfo.BATCH_SIZE_KEY.toLowerCase()))); - impalaTableInfo.setBatchWaitInterval(MathUtil.getLongVal(props.get(ImpalaTableInfo.BATCH_WAIT_INTERVAL_KEY.toLowerCase()))); - impalaTableInfo.setBufferSize(MathUtil.getString(props.get(ImpalaTableInfo.BUFFER_SIZE_KEY.toLowerCase()))); - impalaTableInfo.setFlushIntervalMs(MathUtil.getString(props.get(ImpalaTableInfo.FLUSH_INTERVALMS_KEY.toLowerCase()))); - impalaTableInfo.setSchema(MathUtil.getString(props.get(ImpalaTableInfo.SCHEMA_KEY.toLowerCase()))); - impalaTableInfo.setUpdateMode(MathUtil.getString(props.get(ImpalaTableInfo.UPDATE_KEY.toLowerCase()))); - - Integer authMech = MathUtil.getIntegerVal(props.get(ImpalaTableInfo.AUTHMECH_KEY.toLowerCase())); + impalaTableInfo.setParallelism(MathUtil.getIntegerVal(props.get(PARALLELISM_KEY.toLowerCase()))); + impalaTableInfo.setUrl(MathUtil.getString(props.get(URL_KEY.toLowerCase()))); + impalaTableInfo.setTableName(MathUtil.getString(props.get(TABLE_NAME_KEY.toLowerCase()))); + impalaTableInfo.setBatchSize(MathUtil.getIntegerVal(props.get(BATCH_SIZE_KEY.toLowerCase()))); + impalaTableInfo.setBatchWaitInterval(MathUtil.getLongVal(props.get(BATCH_WAIT_INTERVAL_KEY.toLowerCase()))); + impalaTableInfo.setBufferSize(MathUtil.getString(props.get(BUFFER_SIZE_KEY.toLowerCase()))); + impalaTableInfo.setFlushIntervalMs(MathUtil.getString(props.get(FLUSH_INTERVAL_MS_KEY.toLowerCase()))); + impalaTableInfo.setSchema(MathUtil.getString(props.get(SCHEMA_KEY.toLowerCase()))); + impalaTableInfo.setUpdateMode(MathUtil.getString(props.get(UPDATE_KEY.toLowerCase()))); + + Integer authMech = MathUtil.getIntegerVal(props.get(AUTH_MECH_KEY.toLowerCase())); authMech = authMech == null ? 0 : authMech; impalaTableInfo.setAuthMech(authMech); - List authMechs = Arrays.asList(new Integer[]{0, 1, 2, 3}); + List authMechs = Arrays.asList(0, 1, 2, 3); if (!authMechs.contains(authMech)) { throw new IllegalArgumentException("The value of authMech is illegal, Please select 0, 1, 2, 3"); } else if (authMech == 1) { - impalaTableInfo.setPrincipal(MathUtil.getString(props.get(ImpalaTableInfo.PRINCIPAL_KEY.toLowerCase()))); - impalaTableInfo.setKeyTabFilePath(MathUtil.getString(props.get(ImpalaTableInfo.KEYTABFILEPATH_KEY.toLowerCase()))); - impalaTableInfo.setKrb5FilePath(MathUtil.getString(props.get(ImpalaTableInfo.KRB5FILEPATH_KEY.toLowerCase()))); - String krbRealm = MathUtil.getString(props.get(ImpalaTableInfo.KRBREALM_KEY.toLowerCase())); - krbRealm = krbRealm == null ? "HADOOP.COM" : krbRealm; + impalaTableInfo.setPrincipal(MathUtil.getString(props.get(PRINCIPAL_KEY.toLowerCase()))); + impalaTableInfo.setKeyTabFilePath(MathUtil.getString(props.get(KEY_TAB_FILE_PATH_KEY.toLowerCase()))); + impalaTableInfo.setKrb5FilePath(MathUtil.getString(props.get(KRB5FILEPATH_KEY.toLowerCase()))); + String krbRealm = MathUtil.getString(props.get(KRB_REALM_KEY.toLowerCase())); + krbRealm = krbRealm == null ? KRB_DEFAULT_REALM : krbRealm; impalaTableInfo.setKrbRealm(krbRealm); - impalaTableInfo.setKrbHostFQDN(MathUtil.getString(props.get(ImpalaTableInfo.KRBHOSTFQDN_KEY.toLowerCase()))); - impalaTableInfo.setKrbServiceName(MathUtil.getString(props.get(ImpalaTableInfo.KRBSERVICENAME_KEY.toLowerCase()))); + impalaTableInfo.setKrbHostFQDN(MathUtil.getString(props.get(KRB_HOST_FQDN_KEY.toLowerCase()))); + impalaTableInfo.setKrbServiceName(MathUtil.getString(props.get(KRB_SERVICE_NAME_KEY.toLowerCase()))); } else if (authMech == 2) { - impalaTableInfo.setUserName(MathUtil.getString(props.get(ImpalaTableInfo.USER_NAME_KEY.toLowerCase()))); + impalaTableInfo.setUserName(MathUtil.getString(props.get(USER_NAME_KEY.toLowerCase()))); } else if (authMech == 3) { - impalaTableInfo.setUserName(MathUtil.getString(props.get(ImpalaTableInfo.USER_NAME_KEY.toLowerCase()))); - impalaTableInfo.setPassword(MathUtil.getString(props.get(ImpalaTableInfo.PASSWORD_KEY.toLowerCase()))); + impalaTableInfo.setUserName(MathUtil.getString(props.get(USER_NAME_KEY.toLowerCase()))); + impalaTableInfo.setPassword(MathUtil.getString(props.get(PASSWORD_KEY.toLowerCase()))); } - String enablePartitionStr = (String) props.get(ImpalaTableInfo.ENABLEPARITION_KEY.toLowerCase()); + String storeType = MathUtil.getString(props.get(STORE_TYPE_KEY.toLowerCase())); + impalaTableInfo.setStoreType(storeType); + + String enablePartitionStr = (String) props.get(ENABLE_PARTITION_KEY.toLowerCase()); boolean enablePartition = MathUtil.getBoolean(enablePartitionStr == null ? "false" : enablePartitionStr); impalaTableInfo.setEnablePartition(enablePartition); - if (enablePartition) { - String partitionFields = MathUtil.getString(props.get(ImpalaTableInfo.PARTITIONFIELDS_KEY.toLowerCase())); + + if (!storeType.equalsIgnoreCase(KUDU_TYPE) && enablePartition) { + String partitionFields = MathUtil.getString(props.get(PARTITION_FIELDS_KEY.toLowerCase())); impalaTableInfo.setPartitionFields(partitionFields); } + impalaTableInfo.setType(CURRENT_TYPE); + impalaTableInfo.check(); return impalaTableInfo; } - - @Override - public Class dbTypeConvertToJavaType(String fieldType) { - switch (fieldType.toLowerCase()) { - case "boolean": - return Boolean.class; - case "char": - return Character.class; - case "double": - return Double.class; - case "float": - return Float.class; - case "tinyint": - return Byte.class; - case "smallint": - return Short.class; - case "int": - return Integer.class; - case "bigint": - return Long.class; - case "decimal": - return BigDecimal.class; - case "string": - case "varchar": - return String.class; - case "timestamp": - return Timestamp.class; - default: - break; - } - - throw new RuntimeException("不支持 " + fieldType + " 类型"); - } } diff --git a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/table/ImpalaTableInfo.java b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/table/ImpalaTableInfo.java index 032d9ea18..bc0dbac23 100644 --- a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/table/ImpalaTableInfo.java +++ b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/table/ImpalaTableInfo.java @@ -16,43 +16,46 @@ * limitations under the License. */ - package com.dtstack.flink.sql.sink.impala.table; -import com.dtstack.flink.sql.sink.rdb.table.RdbTableInfo; +import com.dtstack.flink.sql.enums.EUpdateMode; +import com.dtstack.flink.sql.table.AbstractTargetTableInfo; import com.google.common.base.Preconditions; import org.apache.commons.lang3.StringUtils; +import java.util.Objects; + /** - * Date: 2019/11/13 + * Date: 2020/10/14 * Company: www.dtstack.com * - * @author xiuzhu + * @author tiezhu */ +public class ImpalaTableInfo extends AbstractTargetTableInfo { -public class ImpalaTableInfo extends RdbTableInfo { + private static final int MAX_BATCH_SIZE = 10000; - public static final String AUTHMECH_KEY = "authMech"; + private String url; - public static final String KRB5FILEPATH_KEY = "krb5FilePath"; + private String tableName; - public static final String PRINCIPAL_KEY = "principal"; + private String userName; - public static final String KEYTABFILEPATH_KEY = "keyTabFilePath"; + private String password; - public static final String KRBREALM_KEY = "krbRealm"; + private Integer batchSize; - public static final String KRBHOSTFQDN_KEY = "krbHostFQDN"; + private Long batchWaitInterval; - public static final String KRBSERVICENAME_KEY = "krbServiceName"; + private String bufferSize; - public static final String ENABLEPARITION_KEY = "enablePartition"; + private String flushIntervalMs; - public static final String PARTITIONFIELDS_KEY = "partitionFields"; + private String schema; - private static final String CURR_TYPE = "impala"; + private boolean allReplace; - private static final String PARTITION_FIELD_SPLIT_REGEX = ","; + private String updateMode; private Integer authMech; @@ -68,12 +71,90 @@ public class ImpalaTableInfo extends RdbTableInfo { private String krbServiceName; - private boolean enablePartition; + private boolean enablePartition = false; + + private String partitionFields; + + private String storeType; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getTableName() { + return tableName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Integer getBatchSize() { + return batchSize; + } + + public void setBatchSize(Integer batchSize) { + this.batchSize = batchSize; + } + + public Long getBatchWaitInterval() { + return batchWaitInterval; + } + + public void setBatchWaitInterval(Long batchWaitInterval) { + this.batchWaitInterval = batchWaitInterval; + } + + public String getBufferSize() { + return bufferSize; + } + + public void setBufferSize(String bufferSize) { + this.bufferSize = bufferSize; + } + + public String getFlushIntervalMs() { + return flushIntervalMs; + } + + public void setFlushIntervalMs(String flushIntervalMs) { + this.flushIntervalMs = flushIntervalMs; + } + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } - private String[] partitionFields; + public String getUpdateMode() { + return updateMode; + } - public ImpalaTableInfo() { - setType(CURR_TYPE); + public void setUpdateMode(String updateMode) { + this.updateMode = updateMode; } public Integer getAuthMech() { @@ -140,38 +221,49 @@ public void setEnablePartition(boolean enablePartition) { this.enablePartition = enablePartition; } - public String[] getPartitionFields() { + public String getPartitionFields() { return partitionFields; } public void setPartitionFields(String partitionFields) { - this.partitionFields = StringUtils.split(partitionFields, PARTITION_FIELD_SPLIT_REGEX); + this.partitionFields = partitionFields; + } + + public String getStoreType() { + return storeType; + } + + public void setStoreType(String storeType) { + this.storeType = storeType; + } + + @Override + public String getType() { + return super.getType().toLowerCase(); } @Override public boolean check() { - Preconditions.checkNotNull(this.getUrl(), "impala field of url is required"); - Preconditions.checkNotNull(this.getTableName(), "impala field of tableName is required"); - Preconditions.checkNotNull(this.getAuthMech(), "impala field of authMech is required"); - Integer authMech = getAuthMech(); - - if (authMech == 1) { - Preconditions.checkNotNull(this.getKrb5FilePath(), "impala field of krb5FilePath is required"); - Preconditions.checkNotNull(this.getPrincipal(), "impala field of principal is required"); - Preconditions.checkNotNull(this.getKeyTabFilePath(), "impala field of keyTabFilePath is required"); - Preconditions.checkNotNull(this.getKrbHostFQDN(), "impala field of krbHostFQDN is required"); - Preconditions.checkNotNull(this.getKrbServiceName(), "impala field of krbServiceName is required"); - } else if (authMech == 2) { - Preconditions.checkNotNull(this.getUserName(), "impala field of userName is required"); - }else if (authMech == 3) { - Preconditions.checkNotNull(this.getUserName(), "impala field of userName is required"); - Preconditions.checkNotNull(this.getPassword(), "impala field of password is required"); + Preconditions.checkNotNull(url, "impala field of URL is required"); + Preconditions.checkNotNull(tableName, "impala field of tableName is required"); + + if (Objects.nonNull(batchSize)) { + Preconditions.checkArgument(batchSize <= MAX_BATCH_SIZE, "batchSize must be less than " + MAX_BATCH_SIZE); } - if (isEnablePartition()) { - Preconditions.checkArgument(this.getPartitionFields().length > 0, "impala field of partitionFields is required"); + if (StringUtils.equalsIgnoreCase(updateMode, EUpdateMode.UPSERT.name())) { + Preconditions.checkArgument(Objects.nonNull(getPrimaryKeys()) && getPrimaryKeys().size() > 0, "updateMode mode primary is required"); } + if (Objects.nonNull(getPrimaryKeys())) { + getPrimaryKeys().forEach(pk -> { + Preconditions.checkArgument(getFieldList().contains(pk), "primary key " + pk + " not found in sink table field"); + }); + } + + + Preconditions.checkArgument(getFieldList().size() == getFieldExtraInfoList().size(), + "fields and fieldExtraInfoList attributes must be the same length"); return true; } } diff --git a/kudu/kudu-side/kudu-all-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAllReqRow.java b/kudu/kudu-side/kudu-all-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAllReqRow.java index cf7f0dea7..0b30729f8 100644 --- a/kudu/kudu-side/kudu-all-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAllReqRow.java +++ b/kudu/kudu-side/kudu-all-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAllReqRow.java @@ -7,6 +7,7 @@ import com.dtstack.flink.sql.side.AbstractSideTableInfo; import com.dtstack.flink.sql.side.kudu.table.KuduSideTableInfo; import com.dtstack.flink.sql.side.kudu.utils.KuduUtil; +import com.dtstack.flink.sql.util.KrbUtils; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -18,6 +19,7 @@ import org.apache.flink.table.typeutils.TimeIndicatorTypeInfo; import org.apache.flink.types.Row; import org.apache.flink.util.Collector; +import org.apache.hadoop.security.UserGroupInformation; import org.apache.kudu.ColumnSchema; import org.apache.kudu.Schema; import org.apache.kudu.client.KuduClient; @@ -31,6 +33,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.security.PrivilegedAction; import java.sql.SQLException; import java.sql.Timestamp; import java.util.Arrays; @@ -218,24 +222,8 @@ private String buildKey(Map val, List equalFieldList) { private KuduScanner getConn(KuduSideTableInfo tableInfo) { try { if (client == null) { - String kuduMasters = tableInfo.getKuduMasters(); String tableName = tableInfo.getTableName(); - Integer workerCount = tableInfo.getWorkerCount(); - Integer defaultSocketReadTimeoutMs = tableInfo.getDefaultSocketReadTimeoutMs(); - Integer defaultOperationTimeoutMs = tableInfo.getDefaultOperationTimeoutMs(); - - Preconditions.checkNotNull(kuduMasters, "kuduMasters could not be null"); - - KuduClient.KuduClientBuilder kuduClientBuilder = new KuduClient.KuduClientBuilder(kuduMasters); - if (null != workerCount) { - kuduClientBuilder.workerCount(workerCount); - } - - if (null != defaultOperationTimeoutMs) { - kuduClientBuilder.defaultOperationTimeoutMs(defaultOperationTimeoutMs); - } - client = kuduClientBuilder.build(); - + client = getClient(tableInfo); if (!client.tableExists(tableName)) { throw new IllegalArgumentException("Table Open Failed , please check table exists"); } @@ -250,6 +238,36 @@ private KuduScanner getConn(KuduSideTableInfo tableInfo) { } } + private KuduClient getClient(KuduSideTableInfo tableInfo) throws IOException { + String kuduMasters = tableInfo.getKuduMasters(); + Integer workerCount = tableInfo.getWorkerCount(); + Integer defaultOperationTimeoutMs = tableInfo.getDefaultOperationTimeoutMs(); + + Preconditions.checkNotNull(kuduMasters, "kuduMasters could not be null"); + + KuduClient.KuduClientBuilder kuduClientBuilder = new KuduClient.KuduClientBuilder(kuduMasters); + + if (null != workerCount) { + kuduClientBuilder.workerCount(workerCount); + } + + if (null != defaultOperationTimeoutMs) { + kuduClientBuilder.defaultOperationTimeoutMs(defaultOperationTimeoutMs); + } + + if (tableInfo.isEnableKrb()) { + UserGroupInformation ugi = KrbUtils.getUgi(tableInfo.getPrincipal(), tableInfo.getKeytab(), tableInfo.getKrb5conf()); + return ugi.doAs(new PrivilegedAction() { + @Override + public KuduClient run() { + return kuduClientBuilder.build(); + } + }); + } else { + return kuduClientBuilder.build(); + } + } + /** * @param builder 创建AsyncKuduScanner对象 diff --git a/kudu/kudu-side/kudu-async-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAsyncReqRow.java b/kudu/kudu-side/kudu-async-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAsyncReqRow.java index edb49814f..43fda6e9f 100644 --- a/kudu/kudu-side/kudu-async-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAsyncReqRow.java +++ b/kudu/kudu-side/kudu-async-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAsyncReqRow.java @@ -10,6 +10,7 @@ import com.dtstack.flink.sql.side.cache.CacheObj; import com.dtstack.flink.sql.side.kudu.table.KuduSideTableInfo; import com.dtstack.flink.sql.side.kudu.utils.KuduUtil; +import com.dtstack.flink.sql.util.KrbUtils; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.stumbleupon.async.Callback; @@ -23,6 +24,7 @@ import org.apache.flink.table.typeutils.TimeIndicatorTypeInfo; import org.apache.flink.types.Row; import org.apache.flink.util.Preconditions; +import org.apache.hadoop.security.UserGroupInformation; import org.apache.kudu.ColumnSchema; import org.apache.kudu.Schema; import org.apache.kudu.client.AsyncKuduClient; @@ -35,6 +37,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.security.PrivilegedAction; import java.sql.Timestamp; import java.util.*; @@ -80,25 +84,10 @@ public void open(Configuration parameters) throws Exception { * * @throws KuduException */ - private void connKuDu() throws KuduException { + private void connKuDu() throws IOException { if (null == table) { - String kuduMasters = kuduSideTableInfo.getKuduMasters(); String tableName = kuduSideTableInfo.getTableName(); - Integer workerCount = kuduSideTableInfo.getWorkerCount(); - Integer defaultSocketReadTimeoutMs = kuduSideTableInfo.getDefaultSocketReadTimeoutMs(); - Integer defaultOperationTimeoutMs = kuduSideTableInfo.getDefaultOperationTimeoutMs(); - - Preconditions.checkNotNull(kuduMasters, "kuduMasters could not be null"); - - AsyncKuduClient.AsyncKuduClientBuilder asyncKuduClientBuilder = new AsyncKuduClient.AsyncKuduClientBuilder(kuduMasters); - if (null != workerCount) { - asyncKuduClientBuilder.workerCount(workerCount); - } - - if (null != defaultOperationTimeoutMs) { - asyncKuduClientBuilder.defaultOperationTimeoutMs(defaultOperationTimeoutMs); - } - asyncClient = asyncKuduClientBuilder.build(); + asyncClient = getClient(); if (!asyncClient.syncClient().tableExists(tableName)) { throw new IllegalArgumentException("Table Open Failed , please check table exists"); } @@ -128,6 +117,40 @@ private void connKuDu() throws KuduException { scannerBuilder.setProjectedColumnNames(projectColumns); } + private AsyncKuduClient getClient() throws IOException { + String kuduMasters = kuduSideTableInfo.getKuduMasters(); + Integer workerCount = kuduSideTableInfo.getWorkerCount(); + Integer defaultOperationTimeoutMs = kuduSideTableInfo.getDefaultOperationTimeoutMs(); + + Preconditions.checkNotNull(kuduMasters, "kuduMasters could not be null"); + + AsyncKuduClient.AsyncKuduClientBuilder asyncKuduClientBuilder = new AsyncKuduClient.AsyncKuduClientBuilder(kuduMasters); + if (null != workerCount) { + asyncKuduClientBuilder.workerCount(workerCount); + } + + if (null != defaultOperationTimeoutMs) { + asyncKuduClientBuilder.defaultOperationTimeoutMs(defaultOperationTimeoutMs); + } + + if (kuduSideTableInfo.isEnableKrb()) { + UserGroupInformation ugi = KrbUtils.getUgi( + kuduSideTableInfo.getPrincipal(), + kuduSideTableInfo.getKeytab(), + kuduSideTableInfo.getKrb5conf() + ); + return ugi.doAs( + new PrivilegedAction() { + @Override + public AsyncKuduClient run() { + return asyncKuduClientBuilder.build(); + } + }); + } else { + return asyncKuduClientBuilder.build(); + } + } + @Override public void handleAsyncInvoke(Map inputParams, CRow input, ResultFuture resultFuture) throws Exception { CRow inputCopy = new CRow(input.row(), input.change()); diff --git a/kudu/kudu-side/kudu-side-core/src/main/java/com/dtstack/flink/sql/side/kudu/table/KuduSideParser.java b/kudu/kudu-side/kudu-side-core/src/main/java/com/dtstack/flink/sql/side/kudu/table/KuduSideParser.java index 62d215d87..f350dd6c3 100644 --- a/kudu/kudu-side/kudu-side-core/src/main/java/com/dtstack/flink/sql/side/kudu/table/KuduSideParser.java +++ b/kudu/kudu-side/kudu-side-core/src/main/java/com/dtstack/flink/sql/side/kudu/table/KuduSideParser.java @@ -1,5 +1,6 @@ package com.dtstack.flink.sql.side.kudu.table; +import com.dtstack.flink.sql.constrant.PluginParamConsts; import com.dtstack.flink.sql.table.AbstractSideTableParser; import com.dtstack.flink.sql.table.AbstractTableInfo; import com.dtstack.flink.sql.util.MathUtil; @@ -71,6 +72,18 @@ public AbstractTableInfo getTableInfo(String tableName, String fieldsInfo, Map() { + @Override + public AsyncKuduClient run() { + return asyncKuduClientBuilder.build(); + } + }); + } else { + client = asyncKuduClientBuilder.build(); + } + LOG.info("connect kudu is successed!"); + KuduClient syncClient = client.syncClient(); if (syncClient.tableExists(tableName)) { table = syncClient.openTable(tableName); } @@ -215,6 +240,25 @@ public KuduOutputFormatBuilder setDefaultSocketReadTimeoutMs(Integer defaultSock return this; } + public KuduOutputFormatBuilder setPrincipal(String principal) { + kuduOutputFormat.principal = principal; + return this; + } + + public KuduOutputFormatBuilder setKeytab(String keytab) { + kuduOutputFormat.keytab = keytab; + return this; + } + + public KuduOutputFormatBuilder setKrb5conf(String krb5conf) { + kuduOutputFormat.krb5conf = krb5conf; + return this; + } + + public KuduOutputFormatBuilder setEnableKrb(boolean enableKrb) { + kuduOutputFormat.enableKrb = enableKrb; + return this; + } public KuduOutputFormat finish() { if (kuduOutputFormat.kuduMasters == null) { diff --git a/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduSink.java b/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduSink.java index 4c7c3bea8..f8ac125be 100644 --- a/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduSink.java +++ b/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduSink.java @@ -38,6 +38,11 @@ public class KuduSink implements RetractStreamTableSink, Serializable, IStr private int parallelism = -1; + private String principal; + private String keytab; + private String krb5conf; + boolean enableKrb; + @Override public KuduSink genStreamSink(AbstractTargetTableInfo targetTableInfo) { KuduTableInfo kuduTableInfo = (KuduTableInfo) targetTableInfo; @@ -47,12 +52,20 @@ public KuduSink genStreamSink(AbstractTargetTableInfo targetTableInfo) { this.defaultSocketReadTimeoutMs = kuduTableInfo.getDefaultSocketReadTimeoutMs(); this.workerCount = kuduTableInfo.getWorkerCount(); this.writeMode = kuduTableInfo.getWriteMode(); + this.principal = kuduTableInfo.getPrincipal(); + this.keytab = kuduTableInfo.getKeytab(); + this.krb5conf = kuduTableInfo.getKrb5conf(); + this.enableKrb = kuduTableInfo.isEnableKrb(); return this; } @Override public void emitDataStream(DataStream> dataStream) { + consumeDataStream(dataStream); + } + + public DataStreamSink> consumeDataStream(DataStream> dataStream) { KuduOutputFormat.KuduOutputFormatBuilder builder = KuduOutputFormat.buildKuduOutputFormat(); builder.setKuduMasters(this.kuduMasters) .setTableName(this.tableName) @@ -61,7 +74,11 @@ public void emitDataStream(DataStream> dataStream) { .setDefaultOperationTimeoutMs(this.defaultOperationTimeoutMs) .setDefaultSocketReadTimeoutMs(this.defaultSocketReadTimeoutMs) .setFieldNames(this.fieldNames) - .setFieldTypes(this.fieldTypes); + .setFieldTypes(this.fieldTypes) + .setPrincipal(this.principal) + .setKeytab(this.keytab) + .setKrb5conf(this.krb5conf) + .setEnableKrb(this.enableKrb); KuduOutputFormat kuduOutputFormat = builder.finish(); RichSinkFunction richSinkFunction = new OutputFormatSinkFunction(kuduOutputFormat); DataStreamSink dataStreamSink = dataStream.addSink(richSinkFunction); @@ -69,6 +86,7 @@ public void emitDataStream(DataStream> dataStream) { if (parallelism > 0) { dataStreamSink.setParallelism(parallelism); } + return dataStreamSink; } @Override diff --git a/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/table/KuduSinkParser.java b/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/table/KuduSinkParser.java index 20302d44f..80e9cc628 100644 --- a/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/table/KuduSinkParser.java +++ b/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/table/KuduSinkParser.java @@ -1,5 +1,6 @@ package com.dtstack.flink.sql.sink.kudu.table; +import com.dtstack.flink.sql.constrant.PluginParamConsts; import com.dtstack.flink.sql.sink.kudu.KuduOutputFormat; import com.dtstack.flink.sql.table.AbstractTableParser; import com.dtstack.flink.sql.table.AbstractTableInfo; @@ -39,6 +40,18 @@ public AbstractTableInfo getTableInfo(String tableName, String fieldsInfo, Map resultFuture){ + protected void preInvoke(CRow input, ResultFuture resultFuture) { } @@ -120,8 +123,8 @@ protected void preInvoke(CRow input, ResultFuture resultFuture){ public void handleAsyncInvoke(Map inputParams, CRow input, ResultFuture resultFuture) throws Exception { AtomicLong networkLogCounter = new AtomicLong(0L); - while (!connectionStatus.get()){//network is unhealth - if(networkLogCounter.getAndIncrement() % 1000 == 0){ + while (!connectionStatus.get()) {//network is unhealth + if (networkLogCounter.getAndIncrement() % 1000 == 0) { LOG.info("network unhealth to block task"); } Thread.sleep(100); @@ -133,53 +136,84 @@ public void handleAsyncInvoke(Map inputParams, CRow input, Resul private void connectWithRetry(Map inputParams, CRow input, ResultFuture resultFuture, SQLClient rdbSqlClient) { AtomicLong failCounter = new AtomicLong(0); AtomicBoolean finishFlag = new AtomicBoolean(false); - while(!finishFlag.get()){ - try{ + while (!finishFlag.get()) { + try { CountDownLatch latch = new CountDownLatch(1); - rdbSqlClient.getConnection(conn -> { - try { - if(conn.failed()){ - connectionStatus.set(false); - if(failCounter.getAndIncrement() % 1000 == 0){ - LOG.error("getConnection error", conn.cause()); - } - if(failCounter.get() >= sideInfo.getSideTableInfo().getConnectRetryMaxNum(100)){ - resultFuture.completeExceptionally(conn.cause()); - finishFlag.set(true); - } - return; - } - connectionStatus.set(true); - ScheduledFuture timerFuture = registerTimer(input, resultFuture); - cancelTimerWhenComplete(resultFuture, timerFuture); - handleQuery(conn.result(), inputParams, input, resultFuture); - finishFlag.set(true); - } catch (Exception e) { - dealFillDataError(input, resultFuture, e); - } finally { - latch.countDown(); - } - }); + asyncQueryData(inputParams, + input, resultFuture, + rdbSqlClient, + failCounter, + finishFlag, + latch); try { latch.await(); } catch (InterruptedException e) { LOG.error("", e); } - } catch (Exception e){ + } catch (Exception e) { //数据源队列溢出情况 connectionStatus.set(false); } - if(!finishFlag.get()){ + if (!finishFlag.get()) { try { Thread.sleep(3000); - } catch (Exception e){ + } catch (Exception e) { LOG.error("", e); } } } } + protected void asyncQueryData(Map inputParams, + CRow input, + ResultFuture resultFuture, + SQLClient rdbSqlClient, + AtomicLong failCounter, + AtomicBoolean finishFlag, + CountDownLatch latch) { + doAsyncQueryData(inputParams, + input, resultFuture, + rdbSqlClient, + failCounter, + finishFlag, + latch); + } + + final protected void doAsyncQueryData( + Map inputParams, + CRow input, + ResultFuture resultFuture, + SQLClient rdbSqlClient, + AtomicLong failCounter, + AtomicBoolean finishFlag, + CountDownLatch latch) { + rdbSqlClient.getConnection(conn -> { + try { + if (conn.failed()) { + connectionStatus.set(false); + if (failCounter.getAndIncrement() % 1000 == 0) { + LOG.error("getConnection error", conn.cause()); + } + if (failCounter.get() >= sideInfo.getSideTableInfo().getConnectRetryMaxNum(100)) { + resultFuture.completeExceptionally(conn.cause()); + finishFlag.set(true); + } + return; + } + connectionStatus.set(true); + preInvoke(input, resultFuture); + + handleQuery(conn.result(), inputParams, input, resultFuture); + finishFlag.set(true); + } catch (Exception e) { + dealFillDataError(input, resultFuture, e); + } finally { + latch.countDown(); + } + }); + } + private Object convertDataType(Object val) { if (val == null) { @@ -219,7 +253,7 @@ private Object convertDataType(Object val) { @Override public String buildCacheKey(Map inputParam) { - return StringUtils.join(inputParam.values(),"_"); + return StringUtils.join(inputParam.values(), "_"); } @Override @@ -231,7 +265,7 @@ public Row fillData(Row input, Object line) { boolean isTimeIndicatorTypeInfo = TimeIndicatorTypeInfo.class.isAssignableFrom(sideInfo.getRowTypeInfo().getTypeAt(entry.getValue()).getClass()); if (obj instanceof Timestamp && isTimeIndicatorTypeInfo) { //去除上一层OutputRowtimeProcessFunction 调用时区导致的影响 - obj = ((Timestamp) obj).getTime() + (long)LOCAL_TZ.getOffset(((Timestamp) obj).getTime()); + obj = ((Timestamp) obj).getTime() + (long) LOCAL_TZ.getOffset(((Timestamp) obj).getTime()); } row.setField(entry.getKey(), obj); @@ -258,7 +292,7 @@ public void close() throws Exception { rdbSqlClient.close(); } - if(executor != null){ + if (executor != null) { executor.shutdown(); } @@ -268,7 +302,7 @@ public void setRdbSqlClient(SQLClient rdbSqlClient) { this.rdbSqlClient = rdbSqlClient; } - private void handleQuery(SQLConnection connection, Map inputParams, CRow input, ResultFuture resultFuture){ + private void handleQuery(SQLConnection connection, Map inputParams, CRow input, ResultFuture resultFuture) { String key = buildCacheKey(inputParams); JsonArray params = new JsonArray(Lists.newArrayList(inputParams.values())); connection.queryWithParams(sideInfo.getSqlCondition(), params, rs -> { @@ -312,9 +346,9 @@ private void handleQuery(SQLConnection connection, Map inputPara }); } - private Map formatInputParam(Map inputParam){ + private Map formatInputParam(Map inputParam) { Map result = Maps.newHashMap(); - inputParam.forEach((k,v) -> { + inputParam.forEach((k, v) -> { result.put(k, convertDataType(v)); }); return result; diff --git a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/JDBCTypeConvertUtils.java b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/JDBCTypeConvertUtils.java index b1a31de42..f06ecba74 100644 --- a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/JDBCTypeConvertUtils.java +++ b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/JDBCTypeConvertUtils.java @@ -228,4 +228,59 @@ public static int[] buildSqlTypes(List fieldTypeArray) { return tmpFieldsType; } + /** + * according to Java type, get the corresponding SQL type + * + * @param fieldTypeList the Java type + * @return the type number of the corresponding type + */ + public static int[] getSqlTypeFromFieldType(List fieldTypeList) { + int[] tmpFieldsType = new int[fieldTypeList.size()]; + for (int i = 0; i < fieldTypeList.size(); i++) { + String fieldType = fieldTypeList.get(i).toUpperCase(); + switch (fieldType) { + case "INT": + tmpFieldsType[i] = Types.INTEGER; + break; + case "BOOLEAN": + tmpFieldsType[i] = Types.BOOLEAN; + break; + case "BIGINT": + tmpFieldsType[i] = Types.BIGINT; + break; + case "SHORT": + tmpFieldsType[i] = Types.SMALLINT; + break; + case "STRING": + case "CHAR": + tmpFieldsType[i] = Types.CHAR; + break; + case "BYTE": + tmpFieldsType[i] = Types.BINARY; + break; + case "FLOAT": + tmpFieldsType[i] = Types.FLOAT; + break; + case "DOUBLE": + tmpFieldsType[i] = Types.DOUBLE; + break; + case "TIMESTAMP": + tmpFieldsType[i] = Types.TIMESTAMP; + break; + case "BIGDECIMAL": + tmpFieldsType[i] = Types.DECIMAL; + break; + case "DATE": + tmpFieldsType[i] = Types.DATE; + break; + case "TIME": + tmpFieldsType[i] = Types.TIME; + break; + default: + throw new RuntimeException("no support field type for sql. the input type:" + fieldType); + } + } + return tmpFieldsType; + } + } diff --git a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/format/JDBCUpsertOutputFormat.java b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/format/JDBCUpsertOutputFormat.java index b87c2e042..c2491e586 100644 --- a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/format/JDBCUpsertOutputFormat.java +++ b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/format/JDBCUpsertOutputFormat.java @@ -29,19 +29,18 @@ import org.apache.commons.lang3.StringUtils; import org.apache.flink.api.java.tuple.Tuple2; import org.apache.flink.types.Row; +import org.apache.hadoop.security.UserGroupInformation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.sql.ResultSet; +import java.security.PrivilegedAction; import java.sql.SQLException; -import java.sql.Statement; import java.util.List; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import static org.apache.flink.util.Preconditions.checkNotNull; @@ -77,7 +76,6 @@ public class JDBCUpsertOutputFormat extends AbstractJDBCOutputFormat tuple2) throws IOException { - if(!flushFlag.get()){ - throw new RuntimeException("connect exception,can not write record"); - } checkConnectionOpen(); try { if (outRecords.getCount() % RECEIVEDATA_PRINT_FREQUENTY == 0 || LOG.isDebugEnabled()) { @@ -193,7 +190,6 @@ private void checkConnectionOpen() { public synchronized void flush() throws Exception { jdbcWriter.executeBatch(connection); batchCount = 0; - flushFlag.set(true); } /** From 92dd4327c67f78c9625a2629e077e8536955d0f3 Mon Sep 17 00:00:00 2001 From: tiezhu Date: Tue, 27 Oct 2020 09:35:07 +0800 Subject: [PATCH 057/103] [fix] impala varchar data error; storeType npe; value condition bug --- .../sql/sink/impala/ImpalaOutputFormat.java | 17 +++++++++-------- .../sql/sink/impala/table/ImpalaSinkParser.java | 7 +++++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java index 19e904ef2..e880f58d4 100644 --- a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java +++ b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java @@ -76,7 +76,7 @@ public class ImpalaOutputFormat extends AbstractDtRichOutputFormat cast('value' as string) cast(value as timestamp) -> cast('value' as timestamp) private static final Pattern TYPE_PATTERN = Pattern.compile("cast\\((.*) as (.*)\\)"); //specific type which values need to be quoted - private static final String[] NEED_QUOTE_TYPE = {"string", "timestamp"}; + private static final String[] NEED_QUOTE_TYPE = {"string", "timestamp", "varchar"}; private static final Integer DEFAULT_CONN_TIME_OUT = 60; private static final int RECEIVE_DATA_PRINT_FREQUENCY = 1000; @@ -327,10 +327,7 @@ private void putRowIntoMap(Map> rowDataMap, Tuple2 rebuildFieldNameListAndTypeList(List fieldNames, - List staticPartitionFields, - List fieldTypes, - String partitionFields) { + private List rebuildFieldNameListAndTypeList(List fieldNames, List staticPartitionFields, List fieldTypes, String partitionFields) { if (partitionFields.isEmpty()) { return fieldNames; } @@ -364,8 +361,8 @@ private List rebuildFieldNameListAndTypeList(List fieldNames, * @return quoted condition */ private String valueConditionAddQuotation(String valueCondition) { - final String[] valueConditionCopy = {valueCondition}; String[] temps = valueCondition.split(","); + List replacedItem = new ArrayList<>(); Arrays.stream(temps).forEach( item -> { Matcher matcher = TYPE_PATTERN.matcher(item); @@ -375,13 +372,15 @@ private String valueConditionAddQuotation(String valueCondition) { if (Arrays.asList(NEED_QUOTE_TYPE).contains(type)) { if (!"null".equals(value)) { - valueConditionCopy[0] = valueConditionCopy[0].replace(value, "'" + value + "'"); + item = item.replace(value, "'" + value + "'"); } } } + replacedItem.add(item); } ); - return "(" + valueConditionCopy[0] + ")"; + + return "(" + String.join(", ", replacedItem) + ")"; } @Override @@ -746,6 +745,8 @@ public ImpalaOutputFormat build() { checkNotNull(format.password, "password is required!"); } + checkNotNull(format.storeType, "storeType is required!"); + return format; } diff --git a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/table/ImpalaSinkParser.java b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/table/ImpalaSinkParser.java index f1a475f61..003b5a6ab 100644 --- a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/table/ImpalaSinkParser.java +++ b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/table/ImpalaSinkParser.java @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; /** * Date: 2020/10/14 @@ -76,6 +77,8 @@ public class ImpalaSinkParser extends AbstractTableParser { private static final String KUDU_TYPE = "kudu"; + private static final String DEFAULT_STORE_TYPE = "kudu"; + private static final String STORE_TYPE_KEY = "storeType"; private static final String KRB_DEFAULT_REALM = "HADOOP.COM"; @@ -123,13 +126,13 @@ public AbstractTableInfo getTableInfo(String tableName, String fieldsInfo, Map Date: Wed, 28 Oct 2020 14:24:24 +0800 Subject: [PATCH 058/103] [fix] varchar type bug --- .../flink/sql/sink/impala/ImpalaOutputFormat.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java index e880f58d4..ed3370727 100644 --- a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java +++ b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java @@ -52,6 +52,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -370,9 +371,11 @@ private String valueConditionAddQuotation(String valueCondition) { String value = matcher.group(1); String type = matcher.group(2); - if (Arrays.asList(NEED_QUOTE_TYPE).contains(type)) { - if (!"null".equals(value)) { - item = item.replace(value, "'" + value + "'"); + for (String needQuoteType : NEED_QUOTE_TYPE) { + if (type.contains(needQuoteType)) { + if (!"null".equals(value)) { + item = item.replace(value, "'" + value + "'"); + } } } } @@ -580,8 +583,10 @@ private String buildTableFieldsCondition(List fieldNames, String partiti private String buildValuesCondition(List fieldTypes, Row row) { String valuesCondition = fieldTypes.stream().map( f -> { - if (Arrays.asList(NEED_QUOTE_TYPE).contains(f.toLowerCase())) { - return String.format("cast(? as %s)", f.toLowerCase()); + for(String item : NEED_QUOTE_TYPE) { + if (f.toLowerCase().contains(item)) { + return String.format("cast(? as %s)", f.toLowerCase()); + } } return "?"; }).collect(Collectors.joining(", ")); From c0b80769aa5b162b23e1cdb84280170cd22297ba Mon Sep 17 00:00:00 2001 From: tiezhu Date: Wed, 28 Oct 2020 15:08:06 +0800 Subject: [PATCH 059/103] [fix] fix krb bug --- .../java/com/dtstack/flink/sql/util/KrbUtils.java | 13 +++++++++++-- .../com/dtstack/flink/sql/util/KrbUtilsTest.java | 2 +- .../flink/sql/side/impala/ImpalaAllReqRow.java | 2 +- .../flink/sql/side/impala/ImpalaAsyncReqRow.java | 3 +-- .../flink/sql/sink/impala/ImpalaOutputFormat.java | 2 +- .../dtstack/flink/sql/side/kudu/KuduAllReqRow.java | 2 +- .../flink/sql/side/kudu/KuduAsyncReqRow.java | 3 +-- .../flink/sql/sink/kudu/KuduOutputFormat.java | 2 +- 8 files changed, 18 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/util/KrbUtils.java b/core/src/main/java/com/dtstack/flink/sql/util/KrbUtils.java index 2a83359c4..00f2f538d 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/KrbUtils.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/KrbUtils.java @@ -20,8 +20,11 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.security.authentication.util.KerberosName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sun.security.krb5.Config; +import sun.security.krb5.KrbException; import java.io.IOException; @@ -40,9 +43,15 @@ public class KrbUtils { // public static final String FALSE_STR = "false"; // public static final String SUBJECT_ONLY_KEY = "javax.security.auth.useSubjectCredsOnly"; - public static UserGroupInformation getUgi(String principal, String keytabPath, String krb5confPath) throws IOException { - LOG.info("Kerberos login with principal: {} and keytab: {}", principal, keytabPath); + public static UserGroupInformation loginAndReturnUgi(String principal, String keytabPath, String krb5confPath) throws IOException { LOG.info("Kerberos login with principal: {} and keytab: {}", principal, keytabPath); System.setProperty(KRB5_CONF_KEY, krb5confPath); + // 不刷新会读/etc/krb5.conf + try { + Config.refresh(); + KerberosName.resetDefaultRealm(); + } catch (KrbException e) { + LOG.warn("resetting default realm failed, current default realm will still be used.", e); + } // TODO 尚未探索出此选项的意义,以后研究明白方可打开 // System.setProperty(SUBJECT_ONLY_KEY, FALSE_STR); Configuration configuration = new Configuration(); diff --git a/core/src/test/java/com/dtstack/flink/sql/util/KrbUtilsTest.java b/core/src/test/java/com/dtstack/flink/sql/util/KrbUtilsTest.java index 6c7359e14..5d65158c7 100644 --- a/core/src/test/java/com/dtstack/flink/sql/util/KrbUtilsTest.java +++ b/core/src/test/java/com/dtstack/flink/sql/util/KrbUtilsTest.java @@ -35,7 +35,7 @@ public void testGetUgi() throws IOException { String keytabPath = ""; String krb5confPath = ""; try { - KrbUtils.getUgi(principal, keytabPath, krb5confPath); + KrbUtils.loginAndReturnUgi(principal, keytabPath, krb5confPath); } catch (IllegalArgumentException e) { Assert.assertEquals(e.getMessage(), "Can't get Kerberos realm"); } diff --git a/impala/impala-side/impala-all-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAllReqRow.java b/impala/impala-side/impala-all-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAllReqRow.java index 179e52e56..85b1f1b1f 100644 --- a/impala/impala-side/impala-all-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAllReqRow.java +++ b/impala/impala-side/impala-all-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAllReqRow.java @@ -71,7 +71,7 @@ public Connection getConn(String dbUrl, String userName, String password) { String keyTabFilePath = impalaSideTableInfo.getKeyTabFilePath(); String krb5FilePath = impalaSideTableInfo.getKrb5FilePath(); String principal = impalaSideTableInfo.getPrincipal(); - UserGroupInformation ugi = KrbUtils.getUgi(principal, keyTabFilePath, krb5FilePath); + UserGroupInformation ugi = KrbUtils.loginAndReturnUgi(principal, keyTabFilePath, krb5FilePath); connection = ugi.doAs((PrivilegedAction) () -> { try { return DriverManager.getConnection(url); diff --git a/impala/impala-side/impala-async-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAsyncReqRow.java b/impala/impala-side/impala-async-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAsyncReqRow.java index ae8ac2be9..3e7da110a 100644 --- a/impala/impala-side/impala-async-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAsyncReqRow.java +++ b/impala/impala-side/impala-async-side/src/main/java/com/dtstack/flink/sql/side/impala/ImpalaAsyncReqRow.java @@ -33,7 +33,6 @@ import org.apache.flink.configuration.Configuration; import org.apache.flink.streaming.api.functions.async.ResultFuture; import org.apache.flink.table.runtime.types.CRow; -import org.apache.flink.types.Row; import org.apache.hadoop.security.UserGroupInformation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -71,7 +70,7 @@ public void open(Configuration parameters) throws Exception { String keyTabFilePath = impalaSideTableInfo.getKeyTabFilePath(); String krb5FilePath = impalaSideTableInfo.getKrb5FilePath(); String principal = impalaSideTableInfo.getPrincipal(); - ugi = KrbUtils.getUgi(principal, keyTabFilePath, krb5FilePath); + ugi = KrbUtils.loginAndReturnUgi(principal, keyTabFilePath, krb5FilePath); } openJdbc(parameters); } diff --git a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java index ed3370727..a755ee2bf 100644 --- a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java +++ b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java @@ -207,7 +207,7 @@ private void initScheduledTask(Long batchWaitInterval) { private void openConnect() throws IOException { if (authMech == 1) { - UserGroupInformation ugi = KrbUtils.getUgi(principal, keytabPath, krb5confPath); + UserGroupInformation ugi = KrbUtils.loginAndReturnUgi(principal, keytabPath, krb5confPath); try { ugi.doAs((PrivilegedExceptionAction) () -> { openJdbc(); diff --git a/kudu/kudu-side/kudu-all-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAllReqRow.java b/kudu/kudu-side/kudu-all-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAllReqRow.java index 0b30729f8..01ee1cd9b 100644 --- a/kudu/kudu-side/kudu-all-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAllReqRow.java +++ b/kudu/kudu-side/kudu-all-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAllReqRow.java @@ -256,7 +256,7 @@ private KuduClient getClient(KuduSideTableInfo tableInfo) throws IOException { } if (tableInfo.isEnableKrb()) { - UserGroupInformation ugi = KrbUtils.getUgi(tableInfo.getPrincipal(), tableInfo.getKeytab(), tableInfo.getKrb5conf()); + UserGroupInformation ugi = KrbUtils.loginAndReturnUgi(tableInfo.getPrincipal(), tableInfo.getKeytab(), tableInfo.getKrb5conf()); return ugi.doAs(new PrivilegedAction() { @Override public KuduClient run() { diff --git a/kudu/kudu-side/kudu-async-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAsyncReqRow.java b/kudu/kudu-side/kudu-async-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAsyncReqRow.java index 43fda6e9f..84b37ab67 100644 --- a/kudu/kudu-side/kudu-async-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAsyncReqRow.java +++ b/kudu/kudu-side/kudu-async-side/src/main/java/com/dtstack/flink/sql/side/kudu/KuduAsyncReqRow.java @@ -15,7 +15,6 @@ import com.google.common.collect.Maps; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; -import io.vertx.core.json.JsonArray; import org.apache.commons.lang3.StringUtils; import org.apache.flink.api.java.typeutils.RowTypeInfo; import org.apache.flink.configuration.Configuration; @@ -134,7 +133,7 @@ private AsyncKuduClient getClient() throws IOException { } if (kuduSideTableInfo.isEnableKrb()) { - UserGroupInformation ugi = KrbUtils.getUgi( + UserGroupInformation ugi = KrbUtils.loginAndReturnUgi( kuduSideTableInfo.getPrincipal(), kuduSideTableInfo.getKeytab(), kuduSideTableInfo.getKrb5conf() diff --git a/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduOutputFormat.java b/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduOutputFormat.java index 41047762b..ab6ac13a8 100644 --- a/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduOutputFormat.java +++ b/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduOutputFormat.java @@ -116,7 +116,7 @@ private void establishConnection() throws IOException { } if (enableKrb) { - UserGroupInformation ugi = KrbUtils.getUgi( + UserGroupInformation ugi = KrbUtils.loginAndReturnUgi( principal, keytab, krb5conf From 7d943052bd620d9fc4e4d6353f24f67b868d713d Mon Sep 17 00:00:00 2001 From: tiezhu Date: Mon, 2 Nov 2020 19:07:02 +0800 Subject: [PATCH 060/103] =?UTF-8?q?[fix]=20RDB=E5=85=A8=E9=87=8F=E7=BB=B4?= =?UTF-8?q?=E8=A1=A8=E4=BD=BF=E7=94=A8=E5=88=AB=E5=90=8D=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sql/side/rdb/all/AbstractRdbAllReqRow.java | 10 +++++++--- .../flink/sql/side/rdb/all/RdbAllSideInfo.java | 17 +++++++++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java index 0530e57e2..0db947d42 100644 --- a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java +++ b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java @@ -213,13 +213,17 @@ private void queryAndFillData(Map>> tmpCache, C ResultSet resultSet = statement.executeQuery(sql); String[] sideFieldNames = StringUtils.split(sideInfo.getSideSelectFields(), ","); - String[] fields = sideInfo.getSideTableInfo().getFieldTypes(); + String[] sideFieldTypes = sideInfo.getSideTableInfo().getFieldTypes(); + Map sideFieldNamesAndTypes = Maps.newHashMap(); + for (int i = 0; i < sideFieldNames.length; i++) { + sideFieldNamesAndTypes.put(sideFieldNames[i], sideFieldTypes[i]); + } + while (resultSet.next()) { Map oneRow = Maps.newHashMap(); for (String fieldName : sideFieldNames) { Object object = resultSet.getObject(fieldName.trim()); - int fieldIndex = sideInfo.getSideTableInfo().getFieldList().indexOf(fieldName.trim()); - object = SwitchUtil.getTarget(object, fields[fieldIndex]); + object = SwitchUtil.getTarget(object, sideFieldNamesAndTypes.get(fieldName)); oneRow.put(fieldName.trim(), object); } diff --git a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/RdbAllSideInfo.java b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/RdbAllSideInfo.java index 82ec95ca0..cafc715d5 100644 --- a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/RdbAllSideInfo.java +++ b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/RdbAllSideInfo.java @@ -36,6 +36,8 @@ import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; /** @@ -59,7 +61,18 @@ public RdbAllSideInfo(RowTypeInfo rowTypeInfo, JoinInfo joinInfo, List selectFields = Lists.newArrayList(); + Map physicalFields = rdbSideTableInfo.getPhysicalFields(); + physicalFields.keySet().forEach( + item -> { + if (Objects.isNull(physicalFields.get(item))) { + selectFields.add(quoteIdentifier(item)); + } else { + selectFields.add(quoteIdentifier(physicalFields.get(item)) + " AS " + quoteIdentifier(item)); + } + } + ); + sqlCondition = getSelectFromStatement(getTableName(rdbSideTableInfo), selectFields, sideTableInfo.getPredicateInfoes()); LOG.info("--------dimension sql query-------\n{}" + sqlCondition); } @@ -68,7 +81,7 @@ public String getAdditionalWhereClause() { } private String getSelectFromStatement(String tableName, List selectFields, List predicateInfoes) { - String fromClause = selectFields.stream().map(this::quoteIdentifier).collect(Collectors.joining(", ")); + String fromClause = String.join(", ", selectFields); String predicateClause = predicateInfoes.stream().map(this::buildFilterCondition).collect(Collectors.joining(" AND ")); String whereClause = buildWhereClause(predicateClause); return "SELECT " + fromClause + " FROM " + tableName + whereClause; From 845b55e7081f64a11974263948293aeebd5afe9e Mon Sep 17 00:00:00 2001 From: tiezhu Date: Tue, 3 Nov 2020 10:44:31 +0800 Subject: [PATCH 061/103] =?UTF-8?q?[fix]=20=E6=94=AF=E6=8C=81=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E6=88=B3=E8=BD=AC=E5=8C=96=E4=B8=BA=E5=AF=B9=E5=BA=94?= =?UTF-8?q?=E7=9A=84Timestamp=E3=80=81Time=E3=80=81Date=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DtNestRowDeserializationSchema.java | 52 +++++++++++++++++-- .../com/dtstack/flink/sql/util/DateUtil.java | 13 +++++ .../com/dtstack/flink/sql/util/MathUtil.java | 16 +++++- .../side/elasticsearch6/util/MathUtil.java | 38 +++++++++----- .../side/elasticsearch6/util/SwitchUtil.java | 3 +- .../flink/sql/side/rdb/util/SwitchUtil.java | 2 + 6 files changed, 104 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/format/dtnest/DtNestRowDeserializationSchema.java b/core/src/main/java/com/dtstack/flink/sql/format/dtnest/DtNestRowDeserializationSchema.java index 13cc261ad..04679a039 100644 --- a/core/src/main/java/com/dtstack/flink/sql/format/dtnest/DtNestRowDeserializationSchema.java +++ b/core/src/main/java/com/dtstack/flink/sql/format/dtnest/DtNestRowDeserializationSchema.java @@ -42,6 +42,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; /** * source data parse to json format @@ -63,6 +64,15 @@ public class DtNestRowDeserializationSchema extends AbstractDeserializationSchem private final List fieldExtraInfos; private final String charsetName; + private static final Pattern TIMESTAMP_PATTERN = Pattern.compile("^\\d+$"); + private static final Pattern TIMESTAMP_FORMAT_PATTERN = Pattern.compile("\\d+-\\d+-\\d+\\s+\\d+:\\d+:\\d+"); + private static final Pattern TIME_FORMAT_PATTERN = Pattern.compile("\\w+\\d+:\\d+:\\d+"); + private static final Pattern DATE_FORMAT_PATTERN = Pattern.compile("\\w+\\d+-\\d+-\\d+"); + + private static final String TIMESTAMP_TYPE = "timestamp"; + private static final String TIME_TYPE = "time"; + private static final String DATE_TYPE = "date"; + public DtNestRowDeserializationSchema(TypeInformation typeInfo, Map rowAndFieldMapping, List fieldExtraInfos, String charsetName) { @@ -141,14 +151,14 @@ private Object convert(JsonNode node, TypeInformation info) { return node.asText(); } } else if (info.getTypeClass().equals(Types.SQL_DATE.getTypeClass())) { - return Date.valueOf(node.asText()); + return convertToTimestamp(node.asText(), DATE_TYPE); } else if (info.getTypeClass().equals(Types.SQL_TIME.getTypeClass())) { // local zone - return Time.valueOf(node.asText()); + return convertToTimestamp(node.asText(), TIME_TYPE); } else if (info.getTypeClass().equals(Types.SQL_TIMESTAMP.getTypeClass())) { // local zone - return Timestamp.valueOf(node.asText()); - } else if (info instanceof RowTypeInfo) { + return convertToTimestamp(node.asText(), TIMESTAMP_TYPE); + } else if (info instanceof RowTypeInfo) { return convertRow(node, (RowTypeInfo) info); } else if (info instanceof ObjectArrayTypeInfo) { return convertObjectArray(node, ((ObjectArrayTypeInfo) info).getComponentInfo()); @@ -163,6 +173,38 @@ private Object convert(JsonNode node, TypeInformation info) { } } + /** + * 将 2020-09-07 14:49:10.0 和 1598446699685 两种格式都转化为 Timestamp、Time、Date + */ + private static Object convertToTimestamp(String timestamp, String type) { + if (TIMESTAMP_PATTERN.matcher(timestamp).find()) { + switch (type.toLowerCase()) { + case (TIMESTAMP_TYPE): + return new Timestamp(Long.parseLong(timestamp)); + case (DATE_TYPE): + return new Date(new Timestamp(Long.parseLong(timestamp)).getTime()); + case (TIME_TYPE): + return new Time(new Timestamp(Long.parseLong(timestamp)).getTime()); + default: + throw new RuntimeException(String.format("%s transform to %s error!", timestamp, type)); + } + } + + if (TIMESTAMP_FORMAT_PATTERN.matcher(timestamp).find() && TIMESTAMP_TYPE.equalsIgnoreCase(type)) { + return Timestamp.valueOf(timestamp); + } + + if (TIME_FORMAT_PATTERN.matcher(timestamp).find() && TIME_TYPE.equalsIgnoreCase(type)) { + return Time.valueOf(timestamp); + } + + if (DATE_FORMAT_PATTERN.matcher(timestamp).find() && DATE_TYPE.equalsIgnoreCase(type)) { + return Date.valueOf(timestamp); + } + + throw new IllegalArgumentException("Incorrect time format of timestamp, input: " + timestamp); + } + private Row convertTopRow() { Row row = new Row(fieldNames.length); try { @@ -173,7 +215,7 @@ private Row convertTopRow() { if (node == null) { if (fieldExtraInfo != null && fieldExtraInfo.getNotNull()) { throw new IllegalStateException("Failed to find field with name '" - + fieldNames[i] + "'."); + + fieldNames[i] + "'."); } else { row.setField(i, null); } diff --git a/core/src/main/java/com/dtstack/flink/sql/util/DateUtil.java b/core/src/main/java/com/dtstack/flink/sql/util/DateUtil.java index b05de0f16..bb48b0678 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/DateUtil.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/DateUtil.java @@ -59,6 +59,7 @@ public class DateUtil { private static final Pattern DATETIME = Pattern.compile("^\\d{4}-(?:0[0-9]|1[0-2])-[0-9]{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3,9})?Z$"); private static final Pattern DATE = Pattern.compile("^\\d{4}-(?:0[0-9]|1[0-2])-[0-9]{2}$"); + private static final Pattern TIME = Pattern.compile("^\\d{2}:\\d{2}:\\d{2}(\\.\\d{3,9})?Z$"); private static final int MILLIS_PER_SECOND = 1000; @@ -832,4 +833,16 @@ public static java.sql.Date getDateFromStr(String dateStr) { return null == date ? null : new java.sql.Date(date.getTime()); } + public static java.sql.Time getTimeFromStr(String dateStr) { + if (TIME.matcher(dateStr).matches()) { + dateStr = dateStr.substring(0,dateStr.length()-1); + Instant instant = LocalTime.parse(dateStr).atDate(LocalDate.now()).toInstant(ZoneOffset.UTC); + return new java.sql.Time(instant.toEpochMilli()); + } else if (DATETIME.matcher(dateStr).matches()) { + Instant instant = Instant.from(ISO_INSTANT.parse(dateStr)); + return new java.sql.Time(instant.toEpochMilli()); + } + Date date = stringToDate(dateStr); + return null == date ? null : new java.sql.Time(date.getTime()); + } } diff --git a/core/src/main/java/com/dtstack/flink/sql/util/MathUtil.java b/core/src/main/java/com/dtstack/flink/sql/util/MathUtil.java index c5584b5d8..844b87033 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/MathUtil.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/MathUtil.java @@ -22,7 +22,9 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Date; +import java.sql.Time; import java.sql.Timestamp; +import java.util.Objects; /** * Convert val to specified numeric type @@ -234,7 +236,19 @@ public static Date getDate(Object obj) { throw new RuntimeException("not support type of " + obj.getClass() + " convert to Date."); } - + public static Time getTime(Object obj) { + if (Objects.isNull(obj)) { + return null; + } + if (obj instanceof String) { + return DateUtil.getTimeFromStr((String) obj); + } else if (obj instanceof Timestamp) { + return new Time(((Timestamp) obj).getTime()); + } else if (obj instanceof Time) { + return (Time) obj; + } + throw new RuntimeException("not support type of " + obj.getClass() + " convert to Time."); + } public static Timestamp getTimestamp(Object obj) { if (obj == null) { diff --git a/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/MathUtil.java b/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/MathUtil.java index d72530a56..faf256145 100644 --- a/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/MathUtil.java +++ b/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/MathUtil.java @@ -19,22 +19,23 @@ package com.dtstack.flink.sql.side.elasticsearch6.util; +import com.dtstack.flink.sql.util.DateUtil; + import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Date; +import java.sql.Time; import java.sql.Timestamp; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.util.Objects; /** + * Convert val to specified numeric type * Date: 2017/4/21 * Company: www.dtstack.com - * * @author xuchao */ public class MathUtil { - public static Long getLongVal(Object obj) { if (obj == null) { return null; @@ -103,8 +104,6 @@ public static Float getFloatVal(Object obj) { return (Float) obj; } else if (obj instanceof BigDecimal) { return ((BigDecimal) obj).floatValue(); - } else if (obj instanceof Double) { - return ((Double) obj).floatValue(); } throw new RuntimeException("not support type of " + obj.getClass() + " convert to Float."); @@ -126,9 +125,13 @@ public static Double getDoubleVal(Object obj) { if (obj instanceof String) { return Double.valueOf((String) obj); } else if (obj instanceof Float) { + return ((Float) obj).doubleValue(); + } else if (obj instanceof Double) { return (Double) obj; } else if (obj instanceof BigDecimal) { return ((BigDecimal) obj).doubleValue(); + } else if (obj instanceof Integer) { + return ((Integer) obj).doubleValue(); } throw new RuntimeException("not support type of " + obj.getClass() + " convert to Double."); @@ -226,12 +229,7 @@ public static Date getDate(Object obj) { return null; } if (obj instanceof String) { - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - try { - return new Date(format.parse((String) obj).getTime()); - } catch (ParseException e) { - throw new RuntimeException("String convert to Date fail."); - } + return DateUtil.getDateFromStr((String) obj); } else if (obj instanceof Timestamp) { return new Date(((Timestamp) obj).getTime()); } else if (obj instanceof Date) { @@ -240,6 +238,20 @@ public static Date getDate(Object obj) { throw new RuntimeException("not support type of " + obj.getClass() + " convert to Date."); } + public static Time getTime(Object obj) { + if (Objects.isNull(obj)) { + return null; + } + if (obj instanceof String) { + return DateUtil.getTimeFromStr((String) obj); + } else if (obj instanceof Timestamp) { + return new Time(((Timestamp) obj).getTime()); + } else if (obj instanceof Time) { + return (Time) obj; + } + throw new RuntimeException("not support type of " + obj.getClass() + " convert to Time."); + } + public static Timestamp getTimestamp(Object obj) { if (obj == null) { return null; @@ -249,7 +261,7 @@ public static Timestamp getTimestamp(Object obj) { } else if (obj instanceof Date) { return new Timestamp(((Date) obj).getTime()); } else if (obj instanceof String) { - return new Timestamp(getDate(obj).getTime()); + return DateUtil.getTimestampFromStr(obj.toString()); } throw new RuntimeException("not support type of " + obj.getClass() + " convert to Date."); } diff --git a/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/SwitchUtil.java b/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/SwitchUtil.java index e0aaa123e..76165a4a7 100644 --- a/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/SwitchUtil.java +++ b/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/SwitchUtil.java @@ -39,7 +39,6 @@ public static Object getTarget(Object obj, String targetType) { case "int": return MathUtil.getIntegerVal(obj); - case "long": case "bigint": case "bigintunsigned": case "intunsigned": @@ -77,6 +76,8 @@ public static Object getTarget(Object obj, String targetType) { case "timestamp": case "datetime": return MathUtil.getTimestamp(obj); + case "time": + return MathUtil.getTime(obj); default: return obj; } diff --git a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/util/SwitchUtil.java b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/util/SwitchUtil.java index 49d163d4d..62390b47d 100644 --- a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/util/SwitchUtil.java +++ b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/util/SwitchUtil.java @@ -78,6 +78,8 @@ public static Object getTarget(Object obj, String targetType) { case "timestamp": case "datetime": return MathUtil.getTimestamp(obj); + case "time": + return MathUtil.getTime(obj); default: } return obj; From 7872c8072d7b5aac393e9b9e9dbffa37a1a34e3f Mon Sep 17 00:00:00 2001 From: tiezhu Date: Tue, 3 Nov 2020 10:44:31 +0800 Subject: [PATCH 062/103] =?UTF-8?q?[fix]=20=E6=94=AF=E6=8C=81=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E6=88=B3=E8=BD=AC=E5=8C=96=E4=B8=BA=E5=AF=B9=E5=BA=94?= =?UTF-8?q?=E7=9A=84Timestamp=E3=80=81Time=E3=80=81Date=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [fix] 支持时间戳转化为对应的Timestamp、Time、Date时间类型 [fix] 支持时间戳转化为对应的Timestamp、Time、Date时间类型 --- .../DtNestRowDeserializationSchema.java | 62 +++++++++++++++++-- .../com/dtstack/flink/sql/util/DateUtil.java | 13 ++++ .../com/dtstack/flink/sql/util/MathUtil.java | 16 ++++- .../side/elasticsearch6/util/MathUtil.java | 38 ++++++++---- .../side/elasticsearch6/util/SwitchUtil.java | 3 +- .../flink/sql/side/rdb/util/SwitchUtil.java | 2 + 6 files changed, 114 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/format/dtnest/DtNestRowDeserializationSchema.java b/core/src/main/java/com/dtstack/flink/sql/format/dtnest/DtNestRowDeserializationSchema.java index 13cc261ad..8b2ca5f49 100644 --- a/core/src/main/java/com/dtstack/flink/sql/format/dtnest/DtNestRowDeserializationSchema.java +++ b/core/src/main/java/com/dtstack/flink/sql/format/dtnest/DtNestRowDeserializationSchema.java @@ -42,6 +42,8 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * source data parse to json format @@ -63,6 +65,15 @@ public class DtNestRowDeserializationSchema extends AbstractDeserializationSchem private final List fieldExtraInfos; private final String charsetName; + private static final Pattern TIMESTAMP_PATTERN = Pattern.compile("^\\d+$"); + private static final Pattern TIMESTAMP_FORMAT_PATTERN = Pattern.compile("(\\d+-\\d+-\\d+\\s)(\\d+:\\d+:\\d+)"); + private static final Pattern TIME_FORMAT_PATTERN = Pattern.compile("\\w+\\d+:\\d+:\\d+"); + private static final Pattern DATE_FORMAT_PATTERN = Pattern.compile("\\w+\\d+-\\d+-\\d+"); + + private static final String TIMESTAMP_TYPE = "timestamp"; + private static final String TIME_TYPE = "time"; + private static final String DATE_TYPE = "date"; + public DtNestRowDeserializationSchema(TypeInformation typeInfo, Map rowAndFieldMapping, List fieldExtraInfos, String charsetName) { @@ -141,14 +152,14 @@ private Object convert(JsonNode node, TypeInformation info) { return node.asText(); } } else if (info.getTypeClass().equals(Types.SQL_DATE.getTypeClass())) { - return Date.valueOf(node.asText()); + return convertToTimestamp(node.asText(), DATE_TYPE); } else if (info.getTypeClass().equals(Types.SQL_TIME.getTypeClass())) { // local zone - return Time.valueOf(node.asText()); + return convertToTimestamp(node.asText(), TIME_TYPE); } else if (info.getTypeClass().equals(Types.SQL_TIMESTAMP.getTypeClass())) { // local zone - return Timestamp.valueOf(node.asText()); - } else if (info instanceof RowTypeInfo) { + return convertToTimestamp(node.asText(), TIMESTAMP_TYPE); + } else if (info instanceof RowTypeInfo) { return convertRow(node, (RowTypeInfo) info); } else if (info instanceof ObjectArrayTypeInfo) { return convertObjectArray(node, ((ObjectArrayTypeInfo) info).getComponentInfo()); @@ -163,6 +174,47 @@ private Object convert(JsonNode node, TypeInformation info) { } } + /** + * 将 2020-09-07 14:49:10.0 和 1598446699685 两种格式都转化为 Timestamp、Time、Date + */ + private static Object convertToTimestamp(String timestamp, String type) { + if (TIMESTAMP_PATTERN.matcher(timestamp).find()) { + switch (type.toLowerCase()) { + case (TIMESTAMP_TYPE): + return new Timestamp(Long.parseLong(timestamp)); + case (DATE_TYPE): + return new Date(new Timestamp(Long.parseLong(timestamp)).getTime()); + case (TIME_TYPE): + return new Time(new Timestamp(Long.parseLong(timestamp)).getTime()); + default: + throw new RuntimeException(String.format("%s transform to %s error!", timestamp, type)); + } + } + + Matcher matcher = TIMESTAMP_FORMAT_PATTERN.matcher(timestamp); + if (matcher.find()) { + switch (type.toLowerCase()) { + case TIME_TYPE: + return Time.valueOf(String.valueOf(matcher.group(2)).trim()); + case DATE_TYPE: + return Date.valueOf(String.valueOf(matcher.group(1)).trim()); + case TIMESTAMP_TYPE: + return Timestamp.valueOf(timestamp); + default: + } + } + + if (TIME_FORMAT_PATTERN.matcher(timestamp).find() && TIME_TYPE.equalsIgnoreCase(type)) { + return Time.valueOf(timestamp); + } + + if (DATE_FORMAT_PATTERN.matcher(timestamp).find() && DATE_TYPE.equalsIgnoreCase(type)) { + return Date.valueOf(timestamp); + } + + throw new IllegalArgumentException("Incorrect time format of timestamp, input: " + timestamp); + } + private Row convertTopRow() { Row row = new Row(fieldNames.length); try { @@ -173,7 +225,7 @@ private Row convertTopRow() { if (node == null) { if (fieldExtraInfo != null && fieldExtraInfo.getNotNull()) { throw new IllegalStateException("Failed to find field with name '" - + fieldNames[i] + "'."); + + fieldNames[i] + "'."); } else { row.setField(i, null); } diff --git a/core/src/main/java/com/dtstack/flink/sql/util/DateUtil.java b/core/src/main/java/com/dtstack/flink/sql/util/DateUtil.java index b05de0f16..bb48b0678 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/DateUtil.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/DateUtil.java @@ -59,6 +59,7 @@ public class DateUtil { private static final Pattern DATETIME = Pattern.compile("^\\d{4}-(?:0[0-9]|1[0-2])-[0-9]{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3,9})?Z$"); private static final Pattern DATE = Pattern.compile("^\\d{4}-(?:0[0-9]|1[0-2])-[0-9]{2}$"); + private static final Pattern TIME = Pattern.compile("^\\d{2}:\\d{2}:\\d{2}(\\.\\d{3,9})?Z$"); private static final int MILLIS_PER_SECOND = 1000; @@ -832,4 +833,16 @@ public static java.sql.Date getDateFromStr(String dateStr) { return null == date ? null : new java.sql.Date(date.getTime()); } + public static java.sql.Time getTimeFromStr(String dateStr) { + if (TIME.matcher(dateStr).matches()) { + dateStr = dateStr.substring(0,dateStr.length()-1); + Instant instant = LocalTime.parse(dateStr).atDate(LocalDate.now()).toInstant(ZoneOffset.UTC); + return new java.sql.Time(instant.toEpochMilli()); + } else if (DATETIME.matcher(dateStr).matches()) { + Instant instant = Instant.from(ISO_INSTANT.parse(dateStr)); + return new java.sql.Time(instant.toEpochMilli()); + } + Date date = stringToDate(dateStr); + return null == date ? null : new java.sql.Time(date.getTime()); + } } diff --git a/core/src/main/java/com/dtstack/flink/sql/util/MathUtil.java b/core/src/main/java/com/dtstack/flink/sql/util/MathUtil.java index c5584b5d8..844b87033 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/MathUtil.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/MathUtil.java @@ -22,7 +22,9 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Date; +import java.sql.Time; import java.sql.Timestamp; +import java.util.Objects; /** * Convert val to specified numeric type @@ -234,7 +236,19 @@ public static Date getDate(Object obj) { throw new RuntimeException("not support type of " + obj.getClass() + " convert to Date."); } - + public static Time getTime(Object obj) { + if (Objects.isNull(obj)) { + return null; + } + if (obj instanceof String) { + return DateUtil.getTimeFromStr((String) obj); + } else if (obj instanceof Timestamp) { + return new Time(((Timestamp) obj).getTime()); + } else if (obj instanceof Time) { + return (Time) obj; + } + throw new RuntimeException("not support type of " + obj.getClass() + " convert to Time."); + } public static Timestamp getTimestamp(Object obj) { if (obj == null) { diff --git a/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/MathUtil.java b/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/MathUtil.java index d72530a56..faf256145 100644 --- a/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/MathUtil.java +++ b/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/MathUtil.java @@ -19,22 +19,23 @@ package com.dtstack.flink.sql.side.elasticsearch6.util; +import com.dtstack.flink.sql.util.DateUtil; + import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Date; +import java.sql.Time; import java.sql.Timestamp; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.util.Objects; /** + * Convert val to specified numeric type * Date: 2017/4/21 * Company: www.dtstack.com - * * @author xuchao */ public class MathUtil { - public static Long getLongVal(Object obj) { if (obj == null) { return null; @@ -103,8 +104,6 @@ public static Float getFloatVal(Object obj) { return (Float) obj; } else if (obj instanceof BigDecimal) { return ((BigDecimal) obj).floatValue(); - } else if (obj instanceof Double) { - return ((Double) obj).floatValue(); } throw new RuntimeException("not support type of " + obj.getClass() + " convert to Float."); @@ -126,9 +125,13 @@ public static Double getDoubleVal(Object obj) { if (obj instanceof String) { return Double.valueOf((String) obj); } else if (obj instanceof Float) { + return ((Float) obj).doubleValue(); + } else if (obj instanceof Double) { return (Double) obj; } else if (obj instanceof BigDecimal) { return ((BigDecimal) obj).doubleValue(); + } else if (obj instanceof Integer) { + return ((Integer) obj).doubleValue(); } throw new RuntimeException("not support type of " + obj.getClass() + " convert to Double."); @@ -226,12 +229,7 @@ public static Date getDate(Object obj) { return null; } if (obj instanceof String) { - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - try { - return new Date(format.parse((String) obj).getTime()); - } catch (ParseException e) { - throw new RuntimeException("String convert to Date fail."); - } + return DateUtil.getDateFromStr((String) obj); } else if (obj instanceof Timestamp) { return new Date(((Timestamp) obj).getTime()); } else if (obj instanceof Date) { @@ -240,6 +238,20 @@ public static Date getDate(Object obj) { throw new RuntimeException("not support type of " + obj.getClass() + " convert to Date."); } + public static Time getTime(Object obj) { + if (Objects.isNull(obj)) { + return null; + } + if (obj instanceof String) { + return DateUtil.getTimeFromStr((String) obj); + } else if (obj instanceof Timestamp) { + return new Time(((Timestamp) obj).getTime()); + } else if (obj instanceof Time) { + return (Time) obj; + } + throw new RuntimeException("not support type of " + obj.getClass() + " convert to Time."); + } + public static Timestamp getTimestamp(Object obj) { if (obj == null) { return null; @@ -249,7 +261,7 @@ public static Timestamp getTimestamp(Object obj) { } else if (obj instanceof Date) { return new Timestamp(((Date) obj).getTime()); } else if (obj instanceof String) { - return new Timestamp(getDate(obj).getTime()); + return DateUtil.getTimestampFromStr(obj.toString()); } throw new RuntimeException("not support type of " + obj.getClass() + " convert to Date."); } diff --git a/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/SwitchUtil.java b/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/SwitchUtil.java index e0aaa123e..76165a4a7 100644 --- a/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/SwitchUtil.java +++ b/elasticsearch6/elasticsearch6-side/elasticsearch6-side-core/src/main/java/com/dtstack/flink/sql/side/elasticsearch6/util/SwitchUtil.java @@ -39,7 +39,6 @@ public static Object getTarget(Object obj, String targetType) { case "int": return MathUtil.getIntegerVal(obj); - case "long": case "bigint": case "bigintunsigned": case "intunsigned": @@ -77,6 +76,8 @@ public static Object getTarget(Object obj, String targetType) { case "timestamp": case "datetime": return MathUtil.getTimestamp(obj); + case "time": + return MathUtil.getTime(obj); default: return obj; } diff --git a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/util/SwitchUtil.java b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/util/SwitchUtil.java index 49d163d4d..62390b47d 100644 --- a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/util/SwitchUtil.java +++ b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/util/SwitchUtil.java @@ -78,6 +78,8 @@ public static Object getTarget(Object obj, String targetType) { case "timestamp": case "datetime": return MathUtil.getTimestamp(obj); + case "time": + return MathUtil.getTime(obj); default: } return obj; From ab0cbbeeab68bf7e4c44ae5b75fead1d819b8f66 Mon Sep 17 00:00:00 2001 From: tiezhu Date: Tue, 3 Nov 2020 17:15:55 +0800 Subject: [PATCH 063/103] =?UTF-8?q?[fix]=20=E4=BF=AE=E5=A4=8DHbase=20Times?= =?UTF-8?q?tamp=E7=B1=BB=E5=9E=8B=E6=95=B0=E6=8D=AE=E5=86=99=E5=85=A5?= =?UTF-8?q?=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hbase/rowkeydealer/AbstractRowKeyModeDealer.java | 3 --- .../flink/sql/sink/hbase/HbaseOutputFormat.java | 11 +++++------ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/rowkeydealer/AbstractRowKeyModeDealer.java b/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/rowkeydealer/AbstractRowKeyModeDealer.java index 90ee289bd..6b5d6a24f 100644 --- a/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/rowkeydealer/AbstractRowKeyModeDealer.java +++ b/hbase/hbase-side/hbase-async-side/src/main/java/com/dtstack/flink/sql/side/hbase/rowkeydealer/AbstractRowKeyModeDealer.java @@ -93,9 +93,6 @@ protected Row fillData(Row input, Object sideInput){ Row row = new Row(outFieldInfoList.size()); for(Map.Entry entry : inFieldIndex.entrySet()){ Object obj = input.getField(entry.getValue()); - if(obj instanceof Timestamp){ - obj = ((Timestamp)obj).getTime(); - } row.setField(entry.getKey(), obj); } diff --git a/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/HbaseOutputFormat.java b/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/HbaseOutputFormat.java index 6c31bd8dd..42a796403 100644 --- a/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/HbaseOutputFormat.java +++ b/hbase/hbase-sink/src/main/java/com/dtstack/flink/sql/sink/hbase/HbaseOutputFormat.java @@ -189,14 +189,13 @@ private Put getPutByRow(Row record) { Put put = new Put(rowKey.getBytes()); for (int i = 0; i < record.getArity(); ++i) { Object fieldVal = record.getField(i); - byte[] val = null; if (fieldVal != null) { - val = HbaseUtil.toByte(fieldVal); - } - byte[] cf = families[i].getBytes(); - byte[] qualifier = qualifiers[i].getBytes(); + byte[] val = fieldVal.toString().getBytes(); + byte[] cf = families[i].getBytes(); + byte[] qualifier = qualifiers[i].getBytes(); - put.addColumn(cf, qualifier, val); + put.addColumn(cf, qualifier, val); + } } return put; } From ac08db39c56acca87211d4e3d05a77be58b49dbd Mon Sep 17 00:00:00 2001 From: tiezhu Date: Wed, 4 Nov 2020 10:36:40 +0800 Subject: [PATCH 064/103] =?UTF-8?q?[fix]=20=E7=BB=B4=E8=A1=A8JOIN=E5=A4=9A?= =?UTF-8?q?=E4=B8=AA=E5=AD=97=E6=AE=B5=E6=97=A0=E6=95=B0=E6=8D=AE=E5=BC=82?= =?UTF-8?q?=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/dtstack/flink/sql/side/BaseAsyncReqRow.java | 2 +- .../com/dtstack/flink/sql/side/rdb/async/RdbAsyncReqRow.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/side/BaseAsyncReqRow.java b/core/src/main/java/com/dtstack/flink/sql/side/BaseAsyncReqRow.java index 3b6a7f88c..b89eb5615 100644 --- a/core/src/main/java/com/dtstack/flink/sql/side/BaseAsyncReqRow.java +++ b/core/src/main/java/com/dtstack/flink/sql/side/BaseAsyncReqRow.java @@ -181,7 +181,7 @@ public void asyncInvoke(CRow row, ResultFuture resultFuture) throws Except } private Map parseInputParam(CRow input){ - Map inputParams = Maps.newHashMap(); + Map inputParams = Maps.newLinkedHashMap(); for (int i = 0; i < sideInfo.getEqualValIndex().size(); i++) { Integer conValIndex = sideInfo.getEqualValIndex().get(i); Object equalObj = input.row().getField(conValIndex); diff --git a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncReqRow.java b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncReqRow.java index b3274b80b..e8ef2cd73 100644 --- a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncReqRow.java +++ b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncReqRow.java @@ -347,7 +347,7 @@ private void handleQuery(SQLConnection connection, Map inputPara } private Map formatInputParam(Map inputParam) { - Map result = Maps.newHashMap(); + Map result = Maps.newLinkedHashMap(); inputParam.forEach((k, v) -> { result.put(k, convertDataType(v)); }); From f553a3c6d05630ed6ae7b7c052edf33e184753bf Mon Sep 17 00:00:00 2001 From: tiezhu Date: Thu, 5 Nov 2020 16:12:11 +0800 Subject: [PATCH 065/103] =?UTF-8?q?[fix]=20kafka=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=BB=A5'|'=E4=BD=9C=E4=B8=BA=E5=88=86=E5=89=B2=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flink/sql/format/DeserializationMetricWrapper.java | 3 ++- .../com/dtstack/flink/sql/parser/CreateTableParser.java | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/format/DeserializationMetricWrapper.java b/core/src/main/java/com/dtstack/flink/sql/format/DeserializationMetricWrapper.java index 96ebe09ae..a4addc025 100644 --- a/core/src/main/java/com/dtstack/flink/sql/format/DeserializationMetricWrapper.java +++ b/core/src/main/java/com/dtstack/flink/sql/format/DeserializationMetricWrapper.java @@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.nio.charset.StandardCharsets; /** * add metric for source @@ -91,7 +92,7 @@ public void initMetric() { public Row deserialize(byte[] message) throws IOException { try { if (numInRecord.getCount() % dataPrintFrequency == 0) { - LOG.info("receive source data:" + new String(message, "UTF-8")); + LOG.info("receive source data:" + new String(message, StandardCharsets.UTF_8)); } numInRecord.inc(); numInBytes.inc(message.length); diff --git a/core/src/main/java/com/dtstack/flink/sql/parser/CreateTableParser.java b/core/src/main/java/com/dtstack/flink/sql/parser/CreateTableParser.java index 91b1fd2ac..303295751 100644 --- a/core/src/main/java/com/dtstack/flink/sql/parser/CreateTableParser.java +++ b/core/src/main/java/com/dtstack/flink/sql/parser/CreateTableParser.java @@ -72,10 +72,10 @@ public void parseSql(String sql, SqlTree sqlTree) { private Map parseProp(String propsStr){ propsStr = propsStr.replaceAll("'\\s*,", "'|"); - String[] strs = propsStr.trim().split("\\|"); + List strings = DtStringUtil.splitIgnoreQuota(propsStr, '|'); Map propMap = Maps.newHashMap(); - for(int i=0; i ss = DtStringUtil.splitIgnoreQuota(strs[i], '='); + for (String str : strings) { + List ss = DtStringUtil.splitIgnoreQuota(str, '='); String key = ss.get(0).trim(); String value = extractValue(ss.get(1).trim()); propMap.put(key, value); From 3a1836c2c37e29e78597440f3f5e110755d8acbe Mon Sep 17 00:00:00 2001 From: tiezhu Date: Thu, 5 Nov 2020 16:44:37 +0800 Subject: [PATCH 066/103] =?UTF-8?q?[fix]=20=E4=BF=AE=E5=A4=8D=E5=BC=95?= =?UTF-8?q?=E5=8F=B7=E9=87=8C','=E8=A2=AB=E8=A7=A3=E6=9E=90=E5=BC=82?= =?UTF-8?q?=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flink/sql/parser/CreateTableParser.java | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/parser/CreateTableParser.java b/core/src/main/java/com/dtstack/flink/sql/parser/CreateTableParser.java index 303295751..925531240 100644 --- a/core/src/main/java/com/dtstack/flink/sql/parser/CreateTableParser.java +++ b/core/src/main/java/com/dtstack/flink/sql/parser/CreateTableParser.java @@ -16,8 +16,6 @@ * limitations under the License. */ - - package com.dtstack.flink.sql.parser; import com.dtstack.flink.sql.util.DtStringUtil; @@ -41,8 +39,6 @@ public class CreateTableParser implements IParser { private static final Pattern PATTERN = Pattern.compile(PATTERN_STR); - private static final Pattern PROP_PATTERN = Pattern.compile("^'\\s*(.+)\\s*'$"); - public static CreateTableParser newInstance(){ return new CreateTableParser(); } @@ -70,28 +66,19 @@ public void parseSql(String sql, SqlTree sqlTree) { } } - private Map parseProp(String propsStr){ - propsStr = propsStr.replaceAll("'\\s*,", "'|"); - List strings = DtStringUtil.splitIgnoreQuota(propsStr, '|'); + private Map parseProp(String propsStr){ + List strings = DtStringUtil.splitIgnoreQuota(propsStr.trim(), ','); Map propMap = Maps.newHashMap(); for (String str : strings) { List ss = DtStringUtil.splitIgnoreQuota(str, '='); String key = ss.get(0).trim(); - String value = extractValue(ss.get(1).trim()); + String value = ss.get(1).trim().replaceAll("'", "").trim(); propMap.put(key, value); } return propMap; } - private String extractValue(String value) { - Matcher matcher = PROP_PATTERN.matcher(value); - if (matcher.find()) { - return matcher.group(1); - } - throw new RuntimeException("[" + value + "] format is invalid"); - } - public static class SqlParserResult{ private String tableName; From e7610c229a1f36f0e73b406e35e9a6e83cc4091d Mon Sep 17 00:00:00 2001 From: tiezhu Date: Thu, 5 Nov 2020 20:02:10 +0800 Subject: [PATCH 067/103] =?UTF-8?q?[fix]=20add=20jar=20=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E5=8F=8A=E6=B3=A8=E9=87=8A=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/dtstack/flink/sql/parser/SqlParser.java | 15 +++++++-------- .../com/dtstack/flink/sql/util/DtStringUtil.java | 11 +++-------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/parser/SqlParser.java b/core/src/main/java/com/dtstack/flink/sql/parser/SqlParser.java index badcf8d9c..f91aa63c4 100644 --- a/core/src/main/java/com/dtstack/flink/sql/parser/SqlParser.java +++ b/core/src/main/java/com/dtstack/flink/sql/parser/SqlParser.java @@ -16,7 +16,7 @@ * limitations under the License. */ - + package com.dtstack.flink.sql.parser; @@ -28,7 +28,6 @@ import com.google.common.collect.Lists; import com.google.common.base.Strings; -import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.regex.Matcher; @@ -54,7 +53,7 @@ public static void setLocalSqlPluginRoot(String localSqlPluginRoot){ LOCAL_SQL_PLUGIN_ROOT = localSqlPluginRoot; } - private static final Pattern ADD_FIlE_PATTERN = Pattern.compile("(?i).*add\\s+file\\s+.+"); + private static final Pattern ADD_FILE_AND_JAR_PATTERN = Pattern.compile("(?i).*add\\s+file\\s+.+|(?i).*add\\s+jar\\s+.+"); /** * flink support sql syntax @@ -79,7 +78,7 @@ public static SqlTree parseSql(String sql) throws Exception { .replace("\t", " ").trim(); List sqlArr = DtStringUtil.splitIgnoreQuota(sql, SQL_DELIMITER); - sqlArr = removeAddFileStmt(sqlArr); + sqlArr = removeAddFileAndJarStmt(sqlArr); SqlTree sqlTree = new SqlTree(); AbstractTableInfoParser tableInfoParser = new AbstractTableInfoParser(); for(String childSql : sqlArr){ @@ -162,12 +161,12 @@ public static SqlTree parseSql(String sql) throws Exception { } /** - * remove add file with statment etc. add file /etc/krb5.conf; + * remove add file and jar with statment etc. add file /etc/krb5.conf, add jar xxx.jar; */ - private static List removeAddFileStmt(List stmts) { - List cleanedStmts = new ArrayList<>(); + private static List removeAddFileAndJarStmt(List stmts) { + List cleanedStmts = Lists.newArrayList(); for (String stmt : stmts) { - Matcher matcher = ADD_FIlE_PATTERN.matcher(stmt); + Matcher matcher = ADD_FILE_AND_JAR_PATTERN.matcher(stmt); if(!matcher.matches()) { cleanedStmts.add(stmt); } diff --git a/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java b/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java index 3e2ebb82a..f47356c61 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java @@ -167,26 +167,20 @@ public static String replaceIgnoreQuota(String str, String oriStr, String replac public static String dealSqlComment(String sql) { boolean inQuotes = false; boolean inSingleQuotes = false; - int bracketLeftNum = 0; StringBuilder b = new StringBuilder(sql.length()); char[] chars = sql.toCharArray(); for (int index = 0; index < chars.length; index ++) { - if (index == chars.length) { - return b.toString(); - } StringBuilder tempSb = new StringBuilder(2); - if (index > 1) { + if (index >= 1) { tempSb.append(chars[index - 1]); tempSb.append(chars[index]); } - if (tempSb.toString().equals("--")) { + if ("--".equals(tempSb.toString())) { if (inQuotes) { b.append(chars[index]); } else if (inSingleQuotes) { b.append(chars[index]); - } else if (bracketLeftNum > 0) { - b.append(chars[index]); } else { b.deleteCharAt(b.length() - 1); while (chars[index] != '\n') { @@ -210,6 +204,7 @@ public static String dealSqlComment(String sql) { return b.toString(); } + public static String col2string(Object column, String type) { String rowData = column.toString(); ColumnType columnType = ColumnType.valueOf(type.toUpperCase()); From b3de9f1fa9c5cb213ca8c9e5f7b585bd33323eb2 Mon Sep 17 00:00:00 2001 From: tiezhu Date: Fri, 6 Nov 2020 15:09:17 +0800 Subject: [PATCH 068/103] =?UTF-8?q?[opt]=20=E4=BC=98=E5=8C=96impala?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sql/sink/impala/ImpalaOutputFormat.java | 89 ++++++++++--------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java index a755ee2bf..b3271a36f 100644 --- a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java +++ b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java @@ -34,7 +34,6 @@ import org.slf4j.LoggerFactory; import java.io.IOException; -import java.rmi.RemoteException; import java.security.PrivilegedExceptionAction; import java.sql.Connection; import java.sql.DriverManager; @@ -52,7 +51,6 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -77,7 +75,7 @@ public class ImpalaOutputFormat extends AbstractDtRichOutputFormat cast('value' as string) cast(value as timestamp) -> cast('value' as timestamp) private static final Pattern TYPE_PATTERN = Pattern.compile("cast\\((.*) as (.*)\\)"); //specific type which values need to be quoted - private static final String[] NEED_QUOTE_TYPE = {"string", "timestamp", "varchar"}; + private static final String[] NEED_QUOTE_TYPE = {"timestamp", "varchar"}; private static final Integer DEFAULT_CONN_TIME_OUT = 60; private static final int RECEIVE_DATA_PRINT_FREQUENCY = 1000; @@ -162,7 +160,7 @@ public void open(int taskNumber, int numTasks) throws IOException { init(); initMetric(); } catch (Exception e) { - throw new RemoteException("impala output format open error!", e); + throw new RuntimeException("impala output format open error!", e); } } @@ -189,19 +187,23 @@ private void init() throws SQLException { } private void initScheduledTask(Long batchWaitInterval) { - if (batchWaitInterval != 0) { - this.scheduler = new ScheduledThreadPoolExecutor(1, - new DTThreadFactory("impala-upsert-output-format")); - this.scheduledFuture = this.scheduler.scheduleWithFixedDelay(() -> { - synchronized (ImpalaOutputFormat.this) { - try { - flush(); - } catch (Exception e) { - LOG.error("Writing records to impala jdbc failed.", e); - throw new RuntimeException("Writing records to impala jdbc failed.", e); + try { + if (batchWaitInterval != 0) { + this.scheduler = new ScheduledThreadPoolExecutor(1, + new DTThreadFactory("impala-upsert-output-format")); + this.scheduledFuture = this.scheduler.scheduleWithFixedDelay(() -> { + synchronized (ImpalaOutputFormat.this) { + try { + flush(); + } catch (Exception e) { + LOG.error("Writing records to impala jdbc failed.", e); + throw new RuntimeException("Writing records to impala jdbc failed.", e); + } } - } - }, batchWaitInterval, batchWaitInterval, TimeUnit.MILLISECONDS); + }, batchWaitInterval, batchWaitInterval, TimeUnit.MILLISECONDS); + } + } catch (Exception e) { + throw new RuntimeException(e); } } @@ -235,7 +237,7 @@ private void openJdbc() { } } - private synchronized void flush() throws SQLException { + private void flush() throws Exception { if (batchCount > 0) { if (updateMode.equalsIgnoreCase(UPDATE_MODE)) { executeUpdateBatch(); @@ -387,7 +389,7 @@ private String valueConditionAddQuotation(String valueCondition) { } @Override - public synchronized void writeRecord(Tuple2 record) throws IOException { + public void writeRecord(Tuple2 record) throws IOException { try { if (!record.f0) { return; @@ -433,7 +435,7 @@ public synchronized void writeRecord(Tuple2 record) throws IOExcep // Receive data outRecords.inc(); } catch (Exception e) { - throw new RuntimeException("Writing records to impala failed.", e); + throw new IOException("Writing records to impala failed.", e); } } @@ -469,7 +471,7 @@ public void close() throws IOException { updateStatement.close(); } } catch (SQLException e) { - throw new RemoteException("impala connection close failed!"); + throw new RuntimeException("impala connection close failed!", e); } finally { connection = null; statement = null; @@ -489,17 +491,16 @@ public void close() throws IOException { * @param fieldNames field name list * @param partitionFields partition fields * @param rowDataMap row data map - * @throws SQLException throw sql exception */ - private synchronized void executeBatchSql(Statement statement, - String tempSql, - String schema, - String tableName, - String storeType, - Boolean enablePartition, - List fieldNames, - String partitionFields, - Map> rowDataMap) throws SQLException { + private void executeBatchSql(Statement statement, + String tempSql, + String schema, + String tableName, + String storeType, + Boolean enablePartition, + List fieldNames, + String partitionFields, + Map> rowDataMap) { StringBuilder valuesCondition = new StringBuilder(); StringBuilder partitionCondition = new StringBuilder(); String tableFieldsCondition = buildTableFieldsCondition(fieldNames, partitionFields); @@ -510,21 +511,25 @@ private synchronized void executeBatchSql(Statement statement, // kudu ${partitionCondition} is null if (storeType.equalsIgnoreCase(KUDU_TYPE) || !enablePartition) { - rowData = rowDataMap.get(NO_PARTITION); - rowData.forEach(row -> valuesCondition.append(row).append(", ")); - String executeSql = tempSql.replace(VALUES_CONDITION, valuesCondition.toString()) - .replace(PARTITION_CONDITION, partitionCondition.toString()) - .replace(PARTITION_CONSTANT, "") - .replace(TABLE_FIELDS_CONDITION, tableFieldsCondition); - String substring = executeSql.substring(0, executeSql.length() - 2); - statement.execute(substring); + try { + rowData = rowDataMap.get(NO_PARTITION); + rowData.forEach(row -> valuesCondition.append(row).append(", ")); + String executeSql = tempSql.replace(VALUES_CONDITION, valuesCondition.toString()) + .replace(PARTITION_CONDITION, partitionCondition.toString()) + .replace(PARTITION_CONSTANT, "") + .replace(TABLE_FIELDS_CONDITION, tableFieldsCondition); + String substring = executeSql.substring(0, executeSql.length() - 2); + statement.execute(substring); + } catch (Exception e) { + throw new RuntimeException("execute impala SQL error!", e); + } return; } // partition sql Set keySet = rowDataMap.keySet(); String finalTempSql = tempSql; - keySet.forEach(key -> { + for (String key : keySet) { try { String executeSql = String.copyValueOf(finalTempSql.toCharArray()); ArrayList valuesConditionList = rowDataMap.get(key); @@ -535,9 +540,9 @@ private synchronized void executeBatchSql(Statement statement, statement.execute(executeSql); partitionCondition.delete(0, partitionCondition.length()); } catch (SQLException sqlException) { - throw new RuntimeException("execute impala partition SQL error! ", sqlException); + throw new RuntimeException("execute impala SQL error! ", sqlException); } - }); + } } /** @@ -583,7 +588,7 @@ private String buildTableFieldsCondition(List fieldNames, String partiti private String buildValuesCondition(List fieldTypes, Row row) { String valuesCondition = fieldTypes.stream().map( f -> { - for(String item : NEED_QUOTE_TYPE) { + for (String item : NEED_QUOTE_TYPE) { if (f.toLowerCase().contains(item)) { return String.format("cast(? as %s)", f.toLowerCase()); } From d733759d56cd04681b3aeb277187a14d346a029b Mon Sep 17 00:00:00 2001 From: tiezhu Date: Tue, 10 Nov 2020 15:58:34 +0800 Subject: [PATCH 069/103] =?UTF-8?q?[fix-32161]=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BD=BF=E7=94=A8udf=E8=AF=AD=E6=B3=95=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E7=B1=BB=E5=86=B2=E7=AA=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/dtstack/flink/sql/GetPlan.java | 10 ++++++++++ .../dtstack/flink/sql/exec/ExecuteProcessHelper.java | 8 ++++++-- .../java/com/dtstack/flink/sql/exec/ParamsInfo.java | 12 ++++++++++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/GetPlan.java b/core/src/main/java/com/dtstack/flink/sql/GetPlan.java index ba6518b3d..409224577 100644 --- a/core/src/main/java/com/dtstack/flink/sql/GetPlan.java +++ b/core/src/main/java/com/dtstack/flink/sql/GetPlan.java @@ -24,6 +24,9 @@ import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; +import java.net.URL; +import java.net.URLClassLoader; + /** * local模式获取sql任务的执行计划 * Date: 2020/2/17 @@ -33,15 +36,22 @@ public class GetPlan { public static String getExecutionPlan(String[] args) { + ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); + ClassLoader envClassLoader = StreamExecutionEnvironment.class.getClassLoader(); + ClassLoader plannerClassLoader = new URLClassLoader(new URL[0], envClassLoader); try { long start = System.currentTimeMillis(); ParamsInfo paramsInfo = ExecuteProcessHelper.parseParams(args); + paramsInfo.setGetPlan(true); + Thread.currentThread().setContextClassLoader(plannerClassLoader); StreamExecutionEnvironment env = ExecuteProcessHelper.getStreamExecution(paramsInfo); String executionPlan = env.getExecutionPlan(); long end = System.currentTimeMillis(); return ApiResult.createSuccessResultJsonStr(executionPlan, end - start); } catch (Exception e) { return ApiResult.createErrorResultJsonStr(ExceptionUtils.getFullStackTrace(e)); + } finally { + Thread.currentThread().setContextClassLoader(currentClassLoader); } } } diff --git a/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java b/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java index 5692acf1c..cdfacb733 100644 --- a/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java +++ b/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java @@ -159,7 +159,7 @@ public static StreamExecutionEnvironment getStreamExecution(ParamsInfo paramsInf Map registerTableCache = Maps.newHashMap(); //register udf - ExecuteProcessHelper.registerUserDefinedFunction(sqlTree, paramsInfo.getJarUrlList(), tableEnv); + ExecuteProcessHelper.registerUserDefinedFunction(sqlTree, paramsInfo.getJarUrlList(), tableEnv, paramsInfo.isGetPlan()); //register table schema Set classPathSets = ExecuteProcessHelper.registerTable(sqlTree, env, tableEnv, paramsInfo.getLocalSqlPluginPath(), paramsInfo.getRemoteSqlPluginPath(), paramsInfo.getPluginLoadMode(), sideTableMap, registerTableCache); @@ -245,13 +245,17 @@ private static void sqlTranslation(String localSqlPluginPath, } } - public static void registerUserDefinedFunction(SqlTree sqlTree, List jarUrlList, TableEnvironment tableEnv) + public static void registerUserDefinedFunction(SqlTree sqlTree, List jarUrlList, TableEnvironment tableEnv, boolean isGetPlan) throws IllegalAccessException, InvocationTargetException { // udf和tableEnv须由同一个类加载器加载 ClassLoader levelClassLoader = tableEnv.getClass().getClassLoader(); + ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); URLClassLoader classLoader = null; List funcList = sqlTree.getFunctionList(); for (CreateFuncParser.SqlParserResult funcInfo : funcList) { + if (isGetPlan) { + classLoader = ClassLoaderManager.loadExtraJar(jarUrlList, (URLClassLoader) currentClassLoader); + } //classloader if (classLoader == null) { classLoader = ClassLoaderManager.loadExtraJar(jarUrlList, (URLClassLoader) levelClassLoader); diff --git a/core/src/main/java/com/dtstack/flink/sql/exec/ParamsInfo.java b/core/src/main/java/com/dtstack/flink/sql/exec/ParamsInfo.java index 27cc7702d..9619c75c1 100644 --- a/core/src/main/java/com/dtstack/flink/sql/exec/ParamsInfo.java +++ b/core/src/main/java/com/dtstack/flink/sql/exec/ParamsInfo.java @@ -21,6 +21,7 @@ import java.net.URL; import java.util.List; +import java.util.Objects; import java.util.Properties; /** @@ -39,6 +40,7 @@ public class ParamsInfo { private String pluginLoadMode; private String deployMode; private Properties confProp; + private boolean getPlan = false; public ParamsInfo(String sql, String name, List jarUrlList, String localSqlPluginPath, String remoteSqlPluginPath, String pluginLoadMode, String deployMode, Properties confProp) { @@ -52,6 +54,14 @@ public ParamsInfo(String sql, String name, List jarUrlList, String localSql this.confProp = confProp; } + public boolean isGetPlan() { + return getPlan; + } + + public void setGetPlan(boolean getPlan) { + this.getPlan = getPlan; + } + public String getSql() { return sql; } @@ -114,10 +124,8 @@ public static class Builder { private String remoteSqlPluginPath; private String pluginLoadMode; private String deployMode; - private String logLevel; private Properties confProp; - public ParamsInfo.Builder setSql(String sql) { this.sql = sql; return this; From ba0ee350efd4d6356bccae764a91e3657b5b291d Mon Sep 17 00:00:00 2001 From: wuren Date: Thu, 12 Nov 2020 20:46:18 +0800 Subject: [PATCH 070/103] [fix-32327][rdb] make rdb connect pool size smaller --- .../com/dtstack/flink/sql/side/rdb/async/RdbAsyncReqRow.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncReqRow.java b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncReqRow.java index ba37c4843..abd7dd50b 100644 --- a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncReqRow.java +++ b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncReqRow.java @@ -74,7 +74,7 @@ public class RdbAsyncReqRow extends BaseAsyncReqRow { public final static int DEFAULT_DB_CONN_POOL_SIZE = DEFAULT_VERTX_EVENT_LOOP_POOL_SIZE + DEFAULT_VERTX_WORKER_POOL_SIZE; - public final static int MAX_DB_CONN_POOL_SIZE_LIMIT = 20; + public final static int MAX_DB_CONN_POOL_SIZE_LIMIT = 5; public final static int DEFAULT_IDLE_CONNECTION_TEST_PEROID = 60; From 210dc297329ea1847bfab08ca9f93321285c9ab2 Mon Sep 17 00:00:00 2001 From: tiezhu Date: Fri, 13 Nov 2020 14:08:12 +0800 Subject: [PATCH 071/103] =?UTF-8?q?[fix-31988]\=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8Dstring=E7=B1=BB=E5=9E=8B=E8=BD=AC=E5=8C=96=E5=BC=82?= =?UTF-8?q?=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java index b3271a36f..35b0c9a50 100644 --- a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java +++ b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java @@ -75,7 +75,7 @@ public class ImpalaOutputFormat extends AbstractDtRichOutputFormat cast('value' as string) cast(value as timestamp) -> cast('value' as timestamp) private static final Pattern TYPE_PATTERN = Pattern.compile("cast\\((.*) as (.*)\\)"); //specific type which values need to be quoted - private static final String[] NEED_QUOTE_TYPE = {"timestamp", "varchar"}; + private static final String[] NEED_QUOTE_TYPE = {"string", "timestamp", "varchar"}; private static final Integer DEFAULT_CONN_TIME_OUT = 60; private static final int RECEIVE_DATA_PRINT_FREQUENCY = 1000; @@ -759,7 +759,5 @@ public ImpalaOutputFormat build() { return format; } - } - } From 47c4a28b1750164f900430a2c6b017662156bc1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=93=81=E6=9F=B1?= Date: Tue, 17 Nov 2020 14:44:15 +0800 Subject: [PATCH 072/103] =?UTF-8?q?[fix-32324][rdb-all]=E4=BF=AE=E5=A4=8Dr?= =?UTF-8?q?db=E5=85=A8=E9=87=8F=E7=BB=B4=E8=A1=A8=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E5=8C=B9=E9=85=8D=E5=BC=82=E5=B8=B8=EF=BC=8C=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E6=83=85=E5=86=B5=EF=BC=9ADDL=E4=B8=AD=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E4=B8=BAid,=20name,=20age,=20=E5=8F=AASELECT=20id,=20age?= =?UTF-8?q?=E4=B8=A4=E4=B8=AA=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java index 0db947d42..7badb736f 100644 --- a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java +++ b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java @@ -213,10 +213,11 @@ private void queryAndFillData(Map>> tmpCache, C ResultSet resultSet = statement.executeQuery(sql); String[] sideFieldNames = StringUtils.split(sideInfo.getSideSelectFields(), ","); + String[] fields = sideInfo.getSideTableInfo().getFields(); String[] sideFieldTypes = sideInfo.getSideTableInfo().getFieldTypes(); Map sideFieldNamesAndTypes = Maps.newHashMap(); - for (int i = 0; i < sideFieldNames.length; i++) { - sideFieldNamesAndTypes.put(sideFieldNames[i], sideFieldTypes[i]); + for (int i = 0; i < fields.length; i++) { + sideFieldNamesAndTypes.put(fields[i], sideFieldTypes[i]); } while (resultSet.next()) { From d5557748f004967be787df48308eb8502db3d57b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=93=81=E6=9F=B1?= Date: Fri, 27 Nov 2020 10:08:45 +0800 Subject: [PATCH 073/103] [fix-32869][rdb]fix rdb task hangs after connect retry. --- .../com/dtstack/flink/sql/util/JDBCUtils.java | 80 +++++++++++++++++++ .../side/rdb/all/AbstractRdbAllReqRow.java | 46 +++++------ 2 files changed, 99 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/util/JDBCUtils.java b/core/src/main/java/com/dtstack/flink/sql/util/JDBCUtils.java index faab22172..42d7b9356 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/JDBCUtils.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/JDBCUtils.java @@ -19,9 +19,19 @@ package com.dtstack.flink.sql.util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Objects; public class JDBCUtils { + private static final Logger LOG = LoggerFactory.getLogger(JDBCUtils.class); + private static final Object LOCK = new Object(); public static void forName(String clazz, ClassLoader classLoader) { @@ -44,4 +54,74 @@ public synchronized static void forName(String clazz) { throw new RuntimeException(e); } } + + /** + * 关闭连接资源 + * + * @param rs ResultSet + * @param stmt Statement + * @param conn Connection + * @param commit + */ + public static void closeConnectionResource(ResultSet rs, Statement stmt, Connection conn, boolean commit) { + if (Objects.nonNull(rs)) { + try { + rs.close(); + } catch (SQLException e) { + LOG.warn("Close resultSet error: {}", e.getMessage()); + } + } + + if (Objects.nonNull(stmt)) { + try { + stmt.close(); + } catch (SQLException e) { + LOG.warn("Close statement error:{}", e.getMessage()); + } + } + + if (Objects.nonNull(conn)) { + try { + if (commit) { + commit(conn); + } else { + rollBack(conn); + } + + conn.close(); + } catch (SQLException e) { + LOG.warn("Close connection error:{}", e.getMessage()); + } + } + } + + /** + * 手动提交事物 + * + * @param conn Connection + */ + public static void commit(Connection conn) { + try { + if (!conn.isClosed() && !conn.getAutoCommit()) { + conn.commit(); + } + } catch (SQLException e) { + LOG.warn("commit error:{}", e.getMessage()); + } + } + + /** + * 手动回滚事物 + * + * @param conn Connection + */ + public static void rollBack(Connection conn) { + try { + if (!conn.isClosed() && !conn.getAutoCommit()) { + conn.rollback(); + } + } catch (SQLException e) { + LOG.warn("rollBack error:{}", e.getMessage()); + } + } } diff --git a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java index 7badb736f..082784041 100644 --- a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java +++ b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java @@ -18,6 +18,7 @@ package com.dtstack.flink.sql.side.rdb.all; +import com.dtstack.flink.sql.util.JDBCUtils; import org.apache.flink.api.common.typeinfo.TypeInformation; import org.apache.flink.configuration.Configuration; import org.apache.flink.table.runtime.types.CRow; @@ -167,42 +168,32 @@ protected Object dealTimeAttributeType(Class entry, O boolean isTimeIndicatorTypeInfo = TimeIndicatorTypeInfo.class.isAssignableFrom(entry); if (obj instanceof Timestamp && isTimeIndicatorTypeInfo) { //去除上一层OutputRowtimeProcessFunction 调用时区导致的影响 - obj = ((Timestamp) obj).getTime() + (long)LOCAL_TZ.getOffset(((Timestamp) obj).getTime()); + obj = ((Timestamp) obj).getTime() + (long) LOCAL_TZ.getOffset(((Timestamp) obj).getTime()); } return obj; } private void loadData(Map>> tmpCache) throws SQLException { - RdbSideTableInfo tableInfo = (RdbSideTableInfo) sideInfo.getSideTableInfo(); - Connection connection = null; + queryAndFillData(tmpCache, getConnectionWithRetry((RdbSideTableInfo) sideInfo.getSideTableInfo())); + } - try { - for (int i = 0; i < CONN_RETRY_NUM; i++) { + private Connection getConnectionWithRetry(RdbSideTableInfo tableInfo) throws SQLException { + String connInfo = "url:" + tableInfo.getUrl() + "; userName:" + tableInfo.getUserName(); + String errorMsg = null; + for (int i = 0; i < CONN_RETRY_NUM; i++) { + try { + return getConn(tableInfo.getUrl(), tableInfo.getUserName(), tableInfo.getPassword()); + } catch (Exception e) { try { - connection = getConn(tableInfo.getUrl(), tableInfo.getUserName(), tableInfo.getPassword()); - break; - } catch (Exception e) { - if (i == CONN_RETRY_NUM - 1) { - throw new RuntimeException("", e); - } - try { - String connInfo = "url:" + tableInfo.getUrl() + ";userName:" + tableInfo.getUserName() + ",pwd:" + tableInfo.getPassword(); - LOG.warn("get conn fail, wait for 5 sec and try again, connInfo:" + connInfo); - Thread.sleep(5 * 1000); - } catch (InterruptedException e1) { - LOG.error("", e1); - } + LOG.warn("get conn fail, wait for 5 sec and try again, connInfo:" + connInfo); + errorMsg = e.getCause().toString(); + Thread.sleep(5 * 1000); + } catch (InterruptedException e1) { + LOG.error("", e1); } } - queryAndFillData(tmpCache, connection); - } catch (Exception e) { - LOG.error("", e); - throw new SQLException(e); - } finally { - if (connection != null) { - connection.close(); - } } + throw new SQLException("get conn fail. connInfo: " + connInfo + "\ncause by: " + errorMsg); } private void queryAndFillData(Map>> tmpCache, Connection connection) throws SQLException { @@ -213,8 +204,8 @@ private void queryAndFillData(Map>> tmpCache, C ResultSet resultSet = statement.executeQuery(sql); String[] sideFieldNames = StringUtils.split(sideInfo.getSideSelectFields(), ","); - String[] fields = sideInfo.getSideTableInfo().getFields(); String[] sideFieldTypes = sideInfo.getSideTableInfo().getFieldTypes(); + String[] fields = sideInfo.getSideTableInfo().getFields(); Map sideFieldNamesAndTypes = Maps.newHashMap(); for (int i = 0; i < fields.length; i++) { sideFieldNamesAndTypes.put(fields[i], sideFieldTypes[i]); @@ -236,6 +227,7 @@ private void queryAndFillData(Map>> tmpCache, C tmpCache.computeIfAbsent(cacheKey, key -> Lists.newArrayList()) .add(oneRow); } + JDBCUtils.closeConnectionResource(resultSet, statement, connection, false); } public int getFetchSize() { From f22586705647a45a820de883bd1f5e5e2f9e3f05 Mon Sep 17 00:00:00 2001 From: chuixue Date: Tue, 1 Dec 2020 15:55:45 +0800 Subject: [PATCH 074/103] [fix-32989][rdb]rdb sink table can not delete --- .../flink/sql/sink/rdb/writer/AbstractUpsertWriter.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AbstractUpsertWriter.java b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AbstractUpsertWriter.java index bc4f763a5..9328257a5 100644 --- a/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AbstractUpsertWriter.java +++ b/rdb/rdb-sink/src/main/java/com/dtstack/flink/sql/sink/rdb/writer/AbstractUpsertWriter.java @@ -122,7 +122,9 @@ public void addRecord(Tuple2 record) throws SQLException { // we don't need perform a deep copy, because jdbc field are immutable object. Tuple2 tuple2 = objectReuse ? new Tuple2<>(record.f0, Row.copy(record.f1)) : record; // add records to buffer - keyToRows.put(getPrimaryKey(tuple2.f1), tuple2); + if (tuple2.f0) { + keyToRows.put(getPrimaryKey(tuple2.f1), tuple2); + } } @Override From 81e6f8e0ed003e646fc4a36bcb8a6ec092a33203 Mon Sep 17 00:00:00 2001 From: xuchao Date: Thu, 3 Dec 2020 11:13:10 +0800 Subject: [PATCH 075/103] =?UTF-8?q?[fix-32973][core]=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=BB=B4=E8=A1=A8join=20=E5=90=8C=E5=90=8Dfield=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=E9=80=BB=E8=BE=91=E4=B8=8D=E4=B8=80=E8=87=B4=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E5=8F=96=E5=80=BC=E9=94=99=E8=AF=AF=E9=97=AE=E9=A2=98?= =?UTF-8?q?=C2=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flink/sql/side/BaseAsyncReqRow.java | 2 +- .../flink/sql/side/JoinNodeDealer.java | 17 +++++++------ .../dtstack/flink/sql/util/TableUtils.java | 25 ++++++------------- .../sql/side/rdb/async/RdbAsyncReqRow.java | 2 +- 4 files changed, 19 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/side/BaseAsyncReqRow.java b/core/src/main/java/com/dtstack/flink/sql/side/BaseAsyncReqRow.java index ae8c75f7e..5f85e434e 100644 --- a/core/src/main/java/com/dtstack/flink/sql/side/BaseAsyncReqRow.java +++ b/core/src/main/java/com/dtstack/flink/sql/side/BaseAsyncReqRow.java @@ -181,7 +181,7 @@ public void asyncInvoke(CRow row, ResultFuture resultFuture) throws Except } private Map parseInputParam(CRow input){ - Map inputParams = Maps.newHashMap(); + Map inputParams = Maps.newLinkedHashMap(); for (int i = 0; i < sideInfo.getEqualValIndex().size(); i++) { Integer conValIndex = sideInfo.getEqualValIndex().get(i); Object equalObj = input.row().getField(conValIndex); diff --git a/core/src/main/java/com/dtstack/flink/sql/side/JoinNodeDealer.java b/core/src/main/java/com/dtstack/flink/sql/side/JoinNodeDealer.java index 6afc12462..a65a53235 100644 --- a/core/src/main/java/com/dtstack/flink/sql/side/JoinNodeDealer.java +++ b/core/src/main/java/com/dtstack/flink/sql/side/JoinNodeDealer.java @@ -285,7 +285,7 @@ private JoinInfo dealNestJoin(SqlJoin joinNode, SqlBasicCall buildAs = TableUtils.buildAsNodeByJoinInfo(joinInfo, null, null); if(rightIsSide){ - addSideInfoToExeQueue(queueInfo, joinInfo, joinNode, parentSelectList, parentGroupByList, parentWhere, tableRef); + addSideInfoToExeQueue(queueInfo, joinInfo, joinNode, parentSelectList, parentGroupByList, parentWhere, tableRef, fieldRef); } SqlNode newLeftNode = joinNode.getLeft(); @@ -298,7 +298,7 @@ private JoinInfo dealNestJoin(SqlJoin joinNode, //替换leftNode 为新的查询 joinNode.setLeft(buildAs); - replaceSelectAndWhereField(buildAs, leftJoinNode, tableRef, parentSelectList, parentGroupByList, parentWhere); + replaceSelectAndWhereField(buildAs, leftJoinNode, tableRef, fieldRef, parentSelectList, parentGroupByList, parentWhere); } return joinInfo; @@ -321,7 +321,8 @@ public void addSideInfoToExeQueue(Queue queueInfo, SqlNodeList parentSelectList, SqlNodeList parentGroupByList, SqlNode parentWhere, - Map tableRef){ + Map tableRef, + Map fieldRef){ //只处理维表 if(!joinInfo.isRightIsSideTable()){ return; @@ -333,7 +334,7 @@ public void addSideInfoToExeQueue(Queue queueInfo, //替换左表为新的表名称 joinNode.setLeft(buildAs); - replaceSelectAndWhereField(buildAs, leftJoinNode, tableRef, parentSelectList, parentGroupByList, parentWhere); + replaceSelectAndWhereField(buildAs, leftJoinNode, tableRef, fieldRef, parentSelectList, parentGroupByList, parentWhere); } /** @@ -348,6 +349,7 @@ public void addSideInfoToExeQueue(Queue queueInfo, public void replaceSelectAndWhereField(SqlBasicCall buildAs, SqlNode leftJoinNode, Map tableRef, + Map fieldRef, SqlNodeList parentSelectList, SqlNodeList parentGroupByList, SqlNode parentWhere){ @@ -361,23 +363,22 @@ public void replaceSelectAndWhereField(SqlBasicCall buildAs, } //替换select field 中的对应字段 - HashBiMap fieldReplaceRef = HashBiMap.create(); for(SqlNode sqlNode : parentSelectList.getList()){ for(String tbTmp : fromTableNameSet) { - TableUtils.replaceSelectFieldTable(sqlNode, tbTmp, newLeftTableName, fieldReplaceRef); + TableUtils.replaceSelectFieldTable(sqlNode, tbTmp, newLeftTableName, fieldRef); } } //TODO 应该根据上面的查询字段的关联关系来替换 //替换where 中的条件相关 for(String tbTmp : fromTableNameSet){ - TableUtils.replaceWhereCondition(parentWhere, tbTmp, newLeftTableName, fieldReplaceRef); + TableUtils.replaceWhereCondition(parentWhere, tbTmp, newLeftTableName, fieldRef); } if(parentGroupByList != null){ for(SqlNode sqlNode : parentGroupByList.getList()){ for(String tbTmp : fromTableNameSet) { - TableUtils.replaceSelectFieldTable(sqlNode, tbTmp, newLeftTableName, fieldReplaceRef); + TableUtils.replaceSelectFieldTable(sqlNode, tbTmp, newLeftTableName, fieldRef); } } } diff --git a/core/src/main/java/com/dtstack/flink/sql/util/TableUtils.java b/core/src/main/java/com/dtstack/flink/sql/util/TableUtils.java index 2aeb06cf8..30ef8b4b3 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/TableUtils.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/TableUtils.java @@ -301,7 +301,7 @@ public static void getFromTableInfo(SqlNode fromTable, Set tableNameSet) public static void replaceSelectFieldTable(SqlNode selectNode, String oldTbName, String newTbName, - HashBiMap fieldReplaceRef) { + Map fieldReplaceRef) { if (selectNode.getKind() == AS) { SqlNode leftNode = ((SqlBasicCall) selectNode).getOperands()[0]; replaceSelectFieldTable(leftNode, oldTbName, newTbName, fieldReplaceRef); @@ -395,22 +395,13 @@ public static void replaceSelectFieldTable(SqlNode selectNode, private static void replaceOneSelectField(SqlIdentifier sqlIdentifier, String newTbName, String oldTbName, - HashBiMap fieldReplaceRef){ + Map fieldReplaceRef){ SqlIdentifier newField = sqlIdentifier.setName(0, newTbName); String fieldName = sqlIdentifier.names.get(1); - String fieldKey = oldTbName + "_" + fieldName; - - if(!fieldReplaceRef.containsKey(fieldKey)){ - if(fieldReplaceRef.inverse().get(fieldName) != null){ - //换一个名字 - String mappingFieldName = ParseUtils.dealDuplicateFieldName(fieldReplaceRef, fieldName); - newField = newField.setName(1, mappingFieldName); - fieldReplaceRef.put(fieldKey, mappingFieldName); - } else { - fieldReplaceRef.put(fieldKey, fieldName); - } - }else { - newField = newField.setName(1, fieldReplaceRef.get(fieldKey)); + String fieldKey = oldTbName + "." + fieldName; + if(fieldReplaceRef.get(fieldKey) != null){ + String newFieldName = fieldReplaceRef.get(fieldKey).split("\\.")[1]; + newField = newField.setName(1, newFieldName); } sqlIdentifier.assignNamesFrom(newField); @@ -511,7 +502,7 @@ public static String getTargetRefField(Map refFieldMap, String c return preFieldName; } - public static void replaceWhereCondition(SqlNode parentWhere, String oldTbName, String newTbName, HashBiMap fieldReplaceRef){ + public static void replaceWhereCondition(SqlNode parentWhere, String oldTbName, String newTbName, Map fieldReplaceRef){ if(parentWhere == null){ return; @@ -527,7 +518,7 @@ public static void replaceWhereCondition(SqlNode parentWhere, String oldTbName, } } - private static void replaceConditionNode(SqlNode selectNode, String oldTbName, String newTbName, HashBiMap fieldReplaceRef) { + private static void replaceConditionNode(SqlNode selectNode, String oldTbName, String newTbName, Map fieldReplaceRef) { if(selectNode.getKind() == IDENTIFIER){ SqlIdentifier sqlIdentifier = (SqlIdentifier) selectNode; diff --git a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncReqRow.java b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncReqRow.java index abd7dd50b..2bcbd1f28 100644 --- a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncReqRow.java +++ b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/async/RdbAsyncReqRow.java @@ -313,7 +313,7 @@ private void handleQuery(SQLConnection connection, Map inputPara } private Map formatInputParam(Map inputParam){ - Map result = Maps.newHashMap(); + Map result = Maps.newLinkedHashMap(); inputParam.forEach((k,v) -> { result.put(k, convertDataType(v)); }); From a4d1b82af371b66f3e38ec36cb1f67986f729e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=93=81=E6=9F=B1?= Date: Tue, 15 Dec 2020 18:58:35 +0800 Subject: [PATCH 076/103] [fix-33268][core] fix hbase rowKey not available due to remove all single quota. rowKey like 'stu'+'_'+sid+'_'+md5(sid). --- .../java/com/dtstack/flink/sql/parser/CreateTableParser.java | 3 ++- .../main/java/com/dtstack/flink/sql/util/DtStringUtil.java | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/parser/CreateTableParser.java b/core/src/main/java/com/dtstack/flink/sql/parser/CreateTableParser.java index 925531240..1486e8bfa 100644 --- a/core/src/main/java/com/dtstack/flink/sql/parser/CreateTableParser.java +++ b/core/src/main/java/com/dtstack/flink/sql/parser/CreateTableParser.java @@ -20,6 +20,7 @@ import com.dtstack.flink.sql.util.DtStringUtil; import com.google.common.collect.Maps; +import org.apache.commons.lang3.StringUtils; import java.util.List; import java.util.Map; @@ -72,7 +73,7 @@ private Map parseProp(String propsStr){ for (String str : strings) { List ss = DtStringUtil.splitIgnoreQuota(str, '='); String key = ss.get(0).trim(); - String value = ss.get(1).trim().replaceAll("'", "").trim(); + String value = DtStringUtil.removeStartAndEndQuota(ss.get(1).trim()); propMap.put(key, value); } diff --git a/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java b/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java index f47356c61..5af657512 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java @@ -408,4 +408,9 @@ public static String getStartQuote() { public static String getEndQuote() { return "\""; } + + public static String removeStartAndEndQuota(String str) { + String removeStart = StringUtils.removeStart(str, "'"); + return StringUtils.removeEnd(removeStart, "'"); + } } From 834e82da46577c75867de73db0f935c178af121f Mon Sep 17 00:00:00 2001 From: wuren Date: Tue, 29 Dec 2020 10:13:36 +0800 Subject: [PATCH 077/103] [fix-33847][core][kafka] add array and map types support in the kafka sink. --- .../flink/sql/exec/ExecuteProcessHelper.java | 4 +- .../flink/sql/function/FunctionManager.java | 9 ---- .../com/dtstack/flink/sql/util/ClassUtil.java | 6 +++ .../dtstack/flink/sql/util/DataTypeUtils.java | 45 ++++++++++++------- .../sql/sink/kafka/AbstractKafkaSink.java | 16 ++++++- 5 files changed, 50 insertions(+), 30 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java b/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java index 5692acf1c..b7af4822f 100644 --- a/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java +++ b/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java @@ -315,9 +315,7 @@ public static Set registerTable(SqlTree sqlTree, StreamExecutionEnvironment } else if (tableInfo instanceof AbstractTargetTableInfo) { TableSink tableSink = StreamSinkFactory.getTableSink((AbstractTargetTableInfo) tableInfo, localSqlPluginPath); - TypeInformation[] flinkTypes = FunctionManager.transformTypes(tableInfo.getFieldClasses()); - tableEnv.registerTableSink(tableInfo.getName(), tableInfo.getFields(), flinkTypes, tableSink); - + tableEnv.registerTableSink(tableInfo.getName(), tableSink); URL sinkTablePathUrl = PluginUtil.buildSourceAndSinkPathByLoadMode(tableInfo.getType(), AbstractTargetTableInfo.TARGET_SUFFIX, localSqlPluginPath, remoteSqlPluginPath, pluginLoadMode); pluginClassPathSets.add(sinkTablePathUrl); } else if (tableInfo instanceof AbstractSideTableInfo) { diff --git a/core/src/main/java/com/dtstack/flink/sql/function/FunctionManager.java b/core/src/main/java/com/dtstack/flink/sql/function/FunctionManager.java index 360cc82ab..5409ac36f 100644 --- a/core/src/main/java/com/dtstack/flink/sql/function/FunctionManager.java +++ b/core/src/main/java/com/dtstack/flink/sql/function/FunctionManager.java @@ -122,13 +122,4 @@ public static void registerAggregateUDF(String classPath, String funcName, Table } } - - public static TypeInformation[] transformTypes(Class[] fieldTypes) { - TypeInformation[] types = new TypeInformation[fieldTypes.length]; - for (int i = 0; i < fieldTypes.length; i++) { - types[i] = TypeInformation.of(fieldTypes[i]); - } - - return types; - } } diff --git a/core/src/main/java/com/dtstack/flink/sql/util/ClassUtil.java b/core/src/main/java/com/dtstack/flink/sql/util/ClassUtil.java index 21551364f..aa293616f 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/ClassUtil.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/ClassUtil.java @@ -25,6 +25,8 @@ import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; +import java.util.HashMap; +import java.util.Map; /** * Reason: TODO ADD REASON(可选) @@ -41,6 +43,10 @@ public static Class stringConvertClass(String str) { if (lowerStr.startsWith("array")) { return Array.newInstance(Integer.class, 0).getClass(); } + if (lowerStr.startsWith("map")) { + Map m = new HashMap(); + return m.getClass(); + } switch (lowerStr) { case "boolean": diff --git a/core/src/main/java/com/dtstack/flink/sql/util/DataTypeUtils.java b/core/src/main/java/com/dtstack/flink/sql/util/DataTypeUtils.java index 72a8b4b66..bb15b9c45 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/DataTypeUtils.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/DataTypeUtils.java @@ -20,6 +20,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.Lists; +import org.apache.commons.lang3.StringUtils; import org.apache.flink.api.common.typeinfo.TypeInformation; import org.apache.flink.api.java.tuple.Tuple2; import org.apache.flink.api.java.typeutils.RowTypeInfo; @@ -30,6 +31,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.apache.commons.lang3.StringUtils.split; + /** * @program: flink.sql * @author: wuren @@ -39,28 +42,13 @@ public class DataTypeUtils { private final static Pattern COMPOSITE_TYPE_PATTERN = Pattern.compile("(.+?)<(.+)>"); private final static String ARRAY = "ARRAY"; + private final static String MAP = "MAP"; private final static String ROW = "ROW"; private final static char FIELD_DELIMITER = ','; private final static char TYPE_DELIMITER = ' '; private DataTypeUtils() {} - /** - * 现在只支持ARRAY类型后续可以加入 MAP等类型 - * @param compositeTypeString - * @return - */ - public static TypeInformation convertToCompositeType(String compositeTypeString) { - Matcher matcher = matchCompositeType(compositeTypeString); - final String errorMsg = "type " + compositeTypeString + "is not support!"; - Preconditions.checkState(matcher.find(), errorMsg); - - String normalizedType = normalizeType(matcher.group(1)); - Preconditions.checkState(ARRAY.equals(normalizedType), errorMsg); - - return convertToArray(compositeTypeString); - } - /** * 目前ARRAY里只支持ROW和其他基本类型 * @param arrayTypeString @@ -86,6 +74,30 @@ public static TypeInformation convertToArray(String arrayTypeString) { return Types.OBJECT_ARRAY(elementType); } + /** + * 目前Map里只支持基本类型 + * @param mapTypeString + * @return + */ + public static TypeInformation convertToMap(String mapTypeString) { + Matcher matcher = matchCompositeType(mapTypeString); + final String errorMsg = mapTypeString + "convert to map type error!"; + Preconditions.checkState(matcher.find(), errorMsg); + + String normalizedType = normalizeType(matcher.group(1)); + Preconditions.checkState(MAP.equals(normalizedType), errorMsg); + + String kvTypeString = matcher.group(2); + String[] kvTypeStringList = StringUtils.split(kvTypeString, ","); + final String mapTypeErrorMsg = "There can only be key and value two types in map declaration."; + Preconditions.checkState(kvTypeStringList.length == 2, mapTypeErrorMsg); + String keyTypeString = normalizeType(kvTypeStringList[0]); + String valueTypeString = normalizeType(kvTypeStringList[1]); + TypeInformation keyType = convertToAtomicType(keyTypeString); + TypeInformation valueType = convertToAtomicType(valueTypeString); + return Types.MAP(keyType, valueType); + } + /** * 目前ROW里只支持基本类型 * @param rowTypeString @@ -104,6 +116,7 @@ public static RowTypeInfo convertToRow(String rowTypeString) { return new RowTypeInfo(info.f0, info.f1); } + private static Tuple2 genFieldInfo(Iterable fieldInfoStrs) { ArrayList types = Lists.newArrayList(); ArrayList fieldNames = Lists.newArrayList(); diff --git a/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java b/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java index 55d1fa7f8..27699c4f3 100644 --- a/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java +++ b/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java @@ -21,6 +21,7 @@ import com.dtstack.flink.sql.enums.EUpdateMode; import com.dtstack.flink.sql.sink.IStreamSinkGener; import com.dtstack.flink.sql.sink.kafka.table.KafkaSinkTableInfo; +import com.dtstack.flink.sql.util.DataTypeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.flink.api.common.typeinfo.TypeInformation; import org.apache.flink.api.java.tuple.Tuple2; @@ -38,6 +39,7 @@ import org.apache.flink.util.Preconditions; import org.apache.kafka.clients.consumer.ConsumerConfig; +import java.util.HashMap; import java.util.Optional; import java.util.Properties; import java.util.stream.IntStream; @@ -77,11 +79,21 @@ protected Properties getKafkaProperties(KafkaSinkTableInfo KafkaSinkTableInfo) { } return props; } - + // TODO Source有相同的方法日后可以合并 protected TypeInformation[] getTypeInformations(KafkaSinkTableInfo kafka11SinkTableInfo) { + String[] fieldTypes = kafka11SinkTableInfo.getFieldTypes(); Class[] fieldClasses = kafka11SinkTableInfo.getFieldClasses(); TypeInformation[] types = IntStream.range(0, fieldClasses.length) - .mapToObj(i -> TypeInformation.of(fieldClasses[i])) + .mapToObj( + i -> { + if (fieldClasses[i].isArray()) { + return DataTypeUtils.convertToArray(fieldTypes[i]); + } + if (fieldClasses[i] == new HashMap().getClass()) { + return DataTypeUtils.convertToMap(fieldTypes[i]); + } + return TypeInformation.of(fieldClasses[i]); + }) .toArray(TypeInformation[]::new); return types; } From 8b839324ef2b597f6527ad6f3d595381313b08ae Mon Sep 17 00:00:00 2001 From: wuren Date: Tue, 29 Dec 2020 13:47:55 +0800 Subject: [PATCH 078/103] [fix-33847][doc] add kafka sink map type doc. --- docs/plugin/kafkaSink.md | 26 ++++++++++++++++++++++++++ docs/pluginsInfo.md | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/plugin/kafkaSink.md b/docs/plugin/kafkaSink.md index 86b01f3ee..b3a1614bc 100644 --- a/docs/plugin/kafkaSink.md +++ b/docs/plugin/kafkaSink.md @@ -221,3 +221,29 @@ into from MyTable a ``` +## MAP类型示例 +目前Kafka Sink支持Map类型 +```sql +CREATE TABLE ods( + id INT, + name STRING +) WITH ( + ... +); + +CREATE TABLE dwd ( + id INT, + dids MAP> +) WITH ( + type ='kafka', + bootstrapServers ='localhost:9092', + offsetReset ='latest', + groupId='wuren_foo', + topic ='luna_foo', + parallelism ='1' +); + +INSERT INTO dwd + SELECT ods.id, MAP['foo', 1, 'bar', 2] AS dids + FROM ods; +``` diff --git a/docs/pluginsInfo.md b/docs/pluginsInfo.md index 88fda90c9..e9d23cdbd 100644 --- a/docs/pluginsInfo.md +++ b/docs/pluginsInfo.md @@ -1,9 +1,9 @@ ### 1 插件列表 #### 1.1 源表插件 * [kafka 源表插件](plugin/kafkaSource.md) -* [kafka 结果表插件](plugin/kafkaSink.md) #### 1.2 结果表插件 +* [kafka 结果表插件](plugin/kafkaSink.md) * [elasticsearch 结果表插件](plugin/elasticsearchSink.md) * [hbase 结果表插件](plugin/hbaseSink.md) * [mysql 结果表插件](plugin/mysqlSink.md) From cf00b23bf2fb7dfe2e27127cacb9a3f96d70fad3 Mon Sep 17 00:00:00 2001 From: wuren Date: Tue, 29 Dec 2020 21:41:27 +0800 Subject: [PATCH 079/103] [fix-33847][core] fix other sink except kafka table schema not found problem. --- .../com/dtstack/flink/sql/exec/ExecuteProcessHelper.java | 9 ++++++++- .../com/dtstack/flink/sql/function/FunctionManager.java | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java b/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java index b7af4822f..df5c6619b 100644 --- a/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java +++ b/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java @@ -315,7 +315,14 @@ public static Set registerTable(SqlTree sqlTree, StreamExecutionEnvironment } else if (tableInfo instanceof AbstractTargetTableInfo) { TableSink tableSink = StreamSinkFactory.getTableSink((AbstractTargetTableInfo) tableInfo, localSqlPluginPath); - tableEnv.registerTableSink(tableInfo.getName(), tableSink); + // TODO Kafka Sink直接注册,其他的Sink要修复才可以。 + if (tableInfo.getType().startsWith("kafka")) { + tableEnv.registerTableSink(tableInfo.getName(), tableSink); + } else { + TypeInformation[] flinkTypes = FunctionManager.transformTypes(tableInfo.getFieldClasses()); + tableEnv.registerTableSink(tableInfo.getName(), tableInfo.getFields(), flinkTypes, tableSink); + } + URL sinkTablePathUrl = PluginUtil.buildSourceAndSinkPathByLoadMode(tableInfo.getType(), AbstractTargetTableInfo.TARGET_SUFFIX, localSqlPluginPath, remoteSqlPluginPath, pluginLoadMode); pluginClassPathSets.add(sinkTablePathUrl); } else if (tableInfo instanceof AbstractSideTableInfo) { diff --git a/core/src/main/java/com/dtstack/flink/sql/function/FunctionManager.java b/core/src/main/java/com/dtstack/flink/sql/function/FunctionManager.java index 5409ac36f..549276dc2 100644 --- a/core/src/main/java/com/dtstack/flink/sql/function/FunctionManager.java +++ b/core/src/main/java/com/dtstack/flink/sql/function/FunctionManager.java @@ -122,4 +122,12 @@ public static void registerAggregateUDF(String classPath, String funcName, Table } } + public static TypeInformation[] transformTypes(Class[] fieldTypes) { + TypeInformation[] types = new TypeInformation[fieldTypes.length]; + for (int i = 0; i < fieldTypes.length; i++) { + types[i] = TypeInformation.of(fieldTypes[i]); + } + return types; + } + } From d58b2c73a8157eb5b7562ee5e66e497e8b05ae38 Mon Sep 17 00:00:00 2001 From: xiuzhu Date: Thu, 31 Dec 2020 10:19:42 +0800 Subject: [PATCH 080/103] =?UTF-8?q?[hotfix-34172][core]=20fix=20flinkPlann?= =?UTF-8?q?er=20=E5=AF=BC=E8=87=B4=E7=9A=84metaspace=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/dtstack/flink/sql/parser/FlinkPlanner.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/parser/FlinkPlanner.java b/core/src/main/java/com/dtstack/flink/sql/parser/FlinkPlanner.java index 7c76ec2cd..3f48d5049 100644 --- a/core/src/main/java/com/dtstack/flink/sql/parser/FlinkPlanner.java +++ b/core/src/main/java/com/dtstack/flink/sql/parser/FlinkPlanner.java @@ -36,12 +36,8 @@ private FlinkPlanner() { } public static FlinkPlannerImpl createFlinkPlanner(FrameworkConfig frameworkConfig, RelOptPlanner relOptPlanner, FlinkTypeFactory typeFactory) { - if (flinkPlanner == null) { - synchronized (FlinkPlanner.class) { - if (flinkPlanner == null) { - flinkPlanner = new FlinkPlannerImpl(frameworkConfig, relOptPlanner, typeFactory); - } - } + synchronized (FlinkPlanner.class) { + flinkPlanner = new FlinkPlannerImpl(frameworkConfig, relOptPlanner, typeFactory); } return flinkPlanner; } From 7de4d0432cba027dc28058576dd7a186b11334a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=93=81=E6=9F=B1?= Date: Thu, 31 Dec 2020 11:14:36 +0800 Subject: [PATCH 081/103] =?UTF-8?q?[fix-34156][rdb]=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=85=A8=E9=87=8F=E7=BB=B4=E8=A1=A8=E4=B8=BB=E9=94=AE=E5=AD=98?= =?UTF-8?q?=E5=9C=A8null=E5=80=BC=E6=97=B6=E5=87=BA=E7=8E=B0=E7=A9=BA?= =?UTF-8?q?=E6=8C=87=E9=92=88=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java index 0530e57e2..1f8cc2a7f 100644 --- a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java +++ b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java @@ -225,7 +225,7 @@ private void queryAndFillData(Map>> tmpCache, C String cacheKey = sideInfo.getEqualFieldList().stream() .map(oneRow::get) - .map(Object::toString) + .map(String::valueOf) .collect(Collectors.joining("_")); tmpCache.computeIfAbsent(cacheKey, key -> Lists.newArrayList()) From 50dc15b64492bf9a70e5acb4f029a7fb25c6f007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=93=81=E6=9F=B1?= Date: Thu, 31 Dec 2020 11:14:36 +0800 Subject: [PATCH 082/103] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=85=A8=E9=87=8F?= =?UTF-8?q?=E7=BB=B4=E8=A1=A8=E4=B8=BB=E9=94=AE=E5=AD=98=E5=9C=A8null?= =?UTF-8?q?=E5=80=BC=E6=97=B6=E5=87=BA=E7=8E=B0=E7=A9=BA=E6=8C=87=E9=92=88?= =?UTF-8?q?=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java index 082784041..853060755 100644 --- a/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java +++ b/rdb/rdb-side/src/main/java/com/dtstack/flink/sql/side/rdb/all/AbstractRdbAllReqRow.java @@ -221,7 +221,7 @@ private void queryAndFillData(Map>> tmpCache, C String cacheKey = sideInfo.getEqualFieldList().stream() .map(oneRow::get) - .map(Object::toString) + .map(String::valueOf) .collect(Collectors.joining("_")); tmpCache.computeIfAbsent(cacheKey, key -> Lists.newArrayList()) From 1b7936107963d1ebb65a6c1913f0c262c90505b4 Mon Sep 17 00:00:00 2001 From: HiLany Date: Mon, 4 Jan 2021 16:05:02 +0800 Subject: [PATCH 083/103] [fix-33981][elasticsearch6][sink]quota RequestLogger.class source code, adjust log level to debug when httpResponse.getHeaders has warning. --- .../elasticsearch/client/RequestLogger.java | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 elasticsearch6/elasticsearch6-sink/src/main/java/org/elasticsearch/client/RequestLogger.java diff --git a/elasticsearch6/elasticsearch6-sink/src/main/java/org/elasticsearch/client/RequestLogger.java b/elasticsearch6/elasticsearch6-sink/src/main/java/org/elasticsearch/client/RequestLogger.java new file mode 100644 index 000000000..39d79e03a --- /dev/null +++ b/elasticsearch6/elasticsearch6-sink/src/main/java/org/elasticsearch/client/RequestLogger.java @@ -0,0 +1,180 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 org.elasticsearch.client; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.RequestLine; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.BufferedHttpEntity; +import org.apache.http.entity.ContentType; +import org.apache.http.util.EntityUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Quota this source code in 2021-01-04. + * Quota Reason: http://redmine.prod.dtstack.cn/issues/33981 + * Modify Content: adjust log level to debug when httpResponse.getHeaders has warning. + * + * Helper class that exposes static methods to unify the way requests are logged. + * Includes trace logging to log complete requests and responses in curl format. + * Useful for debugging, manually sending logged requests via curl and checking their responses. + * Trace logging is a feature that all the language clients provide. + */ +final class RequestLogger { + + private static final Log tracer = LogFactory.getLog("tracer"); + + private RequestLogger() { + } + + /** + * Logs a request that yielded a response + */ + static void logResponse(Log logger, HttpUriRequest request, HttpHost host, HttpResponse httpResponse) { + if (logger.isDebugEnabled()) { + logger.debug("request [" + request.getMethod() + " " + host + getUri(request.getRequestLine()) + + "] returned [" + httpResponse.getStatusLine() + "]"); + } + // adjust log level to debug when httpResponse.getHeaders has warning. + if (logger.isDebugEnabled()) { + Header[] warnings = httpResponse.getHeaders("Warning"); + if (warnings != null && warnings.length > 0) { + logger.debug(buildWarningMessage(request, host, warnings)); + } + } + if (tracer.isTraceEnabled()) { + String requestLine; + try { + requestLine = buildTraceRequest(request, host); + } catch(IOException e) { + requestLine = ""; + tracer.trace("error while reading request for trace purposes", e); + } + String responseLine; + try { + responseLine = buildTraceResponse(httpResponse); + } catch(IOException e) { + responseLine = ""; + tracer.trace("error while reading response for trace purposes", e); + } + tracer.trace(requestLine + '\n' + responseLine); + } + } + + /** + * Logs a request that failed + */ + static void logFailedRequest(Log logger, HttpUriRequest request, Node node, Exception e) { + if (logger.isDebugEnabled()) { + logger.debug("request [" + request.getMethod() + " " + node.getHost() + getUri(request.getRequestLine()) + "] failed", e); + } + if (tracer.isTraceEnabled()) { + String traceRequest; + try { + traceRequest = buildTraceRequest(request, node.getHost()); + } catch (IOException e1) { + tracer.trace("error while reading request for trace purposes", e); + traceRequest = ""; + } + tracer.trace(traceRequest); + } + } + + static String buildWarningMessage(HttpUriRequest request, HttpHost host, Header[] warnings) { + StringBuilder message = new StringBuilder("request [").append(request.getMethod()).append(" ").append(host) + .append(getUri(request.getRequestLine())).append("] returned ").append(warnings.length).append(" warnings: "); + for (int i = 0; i < warnings.length; i++) { + if (i > 0) { + message.append(","); + } + message.append("[").append(warnings[i].getValue()).append("]"); + } + return message.toString(); + } + + /** + * Creates curl output for given request + */ + static String buildTraceRequest(HttpUriRequest request, HttpHost host) throws IOException { + String requestLine = "curl -iX " + request.getMethod() + " '" + host + getUri(request.getRequestLine()) + "'"; + if (request instanceof HttpEntityEnclosingRequest) { + HttpEntityEnclosingRequest enclosingRequest = (HttpEntityEnclosingRequest) request; + if (enclosingRequest.getEntity() != null) { + requestLine += " -d '"; + HttpEntity entity = enclosingRequest.getEntity(); + if (entity.isRepeatable() == false) { + entity = new BufferedHttpEntity(enclosingRequest.getEntity()); + enclosingRequest.setEntity(entity); + } + requestLine += EntityUtils.toString(entity, StandardCharsets.UTF_8) + "'"; + } + } + return requestLine; + } + + /** + * Creates curl output for given response + */ + static String buildTraceResponse(HttpResponse httpResponse) throws IOException { + StringBuilder responseLine = new StringBuilder(); + responseLine.append("# ").append(httpResponse.getStatusLine()); + for (Header header : httpResponse.getAllHeaders()) { + responseLine.append("\n# ").append(header.getName()).append(": ").append(header.getValue()); + } + responseLine.append("\n#"); + HttpEntity entity = httpResponse.getEntity(); + if (entity != null) { + if (entity.isRepeatable() == false) { + entity = new BufferedHttpEntity(entity); + } + httpResponse.setEntity(entity); + ContentType contentType = ContentType.get(entity); + Charset charset = StandardCharsets.UTF_8; + if (contentType != null && contentType.getCharset() != null) { + charset = contentType.getCharset(); + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(entity.getContent(), charset))) { + String line; + while( (line = reader.readLine()) != null) { + responseLine.append("\n# ").append(line); + } + } + } + return responseLine.toString(); + } + + private static String getUri(RequestLine requestLine) { + if (requestLine.getUri().charAt(0) != '/') { + return "/" + requestLine.getUri(); + } + return requestLine.getUri(); + } +} From ec96b760638b8b248ef9eed0e1981ef57a485ecd Mon Sep 17 00:00:00 2001 From: wuren Date: Tue, 5 Jan 2021 18:41:04 +0800 Subject: [PATCH 084/103] [fix-34344][kafka] remove partition key blank char. --- .../flink/sql/sink/kafka/AbstractKafkaSink.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java b/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java index 27699c4f3..89be2840b 100644 --- a/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java +++ b/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java @@ -39,6 +39,7 @@ import org.apache.flink.util.Preconditions; import org.apache.kafka.clients.consumer.ConsumerConfig; +import java.util.Arrays; import java.util.HashMap; import java.util.Optional; import java.util.Properties; @@ -128,8 +129,13 @@ public CRowTypeInfo getRowTypeInfo() { } protected String[] getPartitionKeys(KafkaSinkTableInfo kafkaSinkTableInfo) { - if (StringUtils.isNotBlank(kafkaSinkTableInfo.getPartitionKeys())) { - return StringUtils.split(kafkaSinkTableInfo.getPartitionKeys(), ','); + String keysStr = kafkaSinkTableInfo.getPartitionKeys(); + if (StringUtils.isNotBlank(keysStr)) { + String[] keys = keysStr.split(","); + String[] cleanedKeys = Arrays.stream(keys) + .map(x -> x.trim()) + .toArray(String[]::new); + return cleanedKeys; } return null; } From 410c5e0e7c39e25930f1db2858250dbfb912326a Mon Sep 17 00:00:00 2001 From: wuren Date: Tue, 5 Jan 2021 19:31:54 +0800 Subject: [PATCH 085/103] [fix-34344][kafka] use apache common split --- .../com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java b/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java index 89be2840b..0a27cb074 100644 --- a/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java +++ b/kafka-base/kafka-base-sink/src/main/java/com/dtstack/flink/sql/sink/kafka/AbstractKafkaSink.java @@ -131,7 +131,7 @@ public CRowTypeInfo getRowTypeInfo() { protected String[] getPartitionKeys(KafkaSinkTableInfo kafkaSinkTableInfo) { String keysStr = kafkaSinkTableInfo.getPartitionKeys(); if (StringUtils.isNotBlank(keysStr)) { - String[] keys = keysStr.split(","); + String[] keys = StringUtils.split(keysStr, ","); String[] cleanedKeys = Arrays.stream(keys) .map(x -> x.trim()) .toArray(String[]::new); From 69e9b1fb5ee4a8039002341394b45f9fd3bc1c08 Mon Sep 17 00:00:00 2001 From: wuren Date: Tue, 5 Jan 2021 21:01:04 +0800 Subject: [PATCH 086/103] [fix-34193][root][pom] delete plugins dir in maven clean phase --- pom.xml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pom.xml b/pom.xml index d10954d3e..f2e85ea33 100644 --- a/pom.xml +++ b/pom.xml @@ -137,6 +137,25 @@ + + maven-antrun-plugin + 1.8 + + + clean-plugins + + clean + + run + + + + + + + + + From 708d9235e40e978da7234a058f705e2cd22e697a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=93=81=E6=9F=B1?= Date: Wed, 6 Jan 2021 16:08:05 +0800 Subject: [PATCH 087/103] =?UTF-8?q?[opt-34257][kudu]=20=E4=BC=98=E5=8C=96k?= =?UTF-8?q?udu=E6=8F=92=E4=BB=B6=EF=BC=8C=E6=94=B9=E7=94=A8KuduClient=20AP?= =?UTF-8?q?I=E4=BB=A3=E6=9B=BFAsyncKuduClient?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flink/sql/sink/kudu/KuduOutputFormat.java | 272 +++++++++--------- .../dtstack/flink/sql/sink/kudu/KuduSink.java | 21 +- 2 files changed, 140 insertions(+), 153 deletions(-) diff --git a/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduOutputFormat.java b/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduOutputFormat.java index ab6ac13a8..e8b508f1f 100644 --- a/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduOutputFormat.java +++ b/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduOutputFormat.java @@ -25,10 +25,9 @@ import org.apache.flink.configuration.Configuration; import org.apache.flink.types.Row; import org.apache.hadoop.security.UserGroupInformation; -import org.apache.kudu.client.AsyncKuduClient; -import org.apache.kudu.client.AsyncKuduSession; import org.apache.kudu.client.KuduClient; import org.apache.kudu.client.KuduException; +import org.apache.kudu.client.KuduSession; import org.apache.kudu.client.KuduTable; import org.apache.kudu.client.Operation; import org.apache.kudu.client.PartialRow; @@ -40,40 +39,29 @@ import java.security.PrivilegedAction; import java.sql.Timestamp; import java.util.Date; +import java.util.Objects; /** - * @author gituser - * @modify xiuzhu + * @author gituser + * @modify xiuzhu */ public class KuduOutputFormat extends AbstractDtRichOutputFormat { private static final long serialVersionUID = 1L; private static final Logger LOG = LoggerFactory.getLogger(KuduOutputFormat.class); - - public enum WriteMode { - // insert - INSERT, - // update - UPDATE, - // update or insert - UPSERT - } - + protected String[] fieldNames; + TypeInformation[] fieldTypes; + boolean enableKrb; private String kuduMasters; - private String tableName; - private WriteMode writeMode; - - protected String[] fieldNames; - - TypeInformation[] fieldTypes; - - private AsyncKuduClient client; + private KuduClient client; private KuduTable table; + private volatile KuduSession session; + private Integer workerCount; private Integer defaultOperationTimeoutMs; @@ -86,11 +74,14 @@ public enum WriteMode { private String principal; private String keytab; private String krb5conf; - boolean enableKrb; private KuduOutputFormat() { } + public static KuduOutputFormatBuilder buildKuduOutputFormat() { + return new KuduOutputFormatBuilder(); + } + @Override public void configure(Configuration parameters) { @@ -103,16 +94,16 @@ public void open(int taskNumber, int numTasks) throws IOException { } private void establishConnection() throws IOException { - AsyncKuduClient.AsyncKuduClientBuilder asyncKuduClientBuilder = new AsyncKuduClient.AsyncKuduClientBuilder(kuduMasters); + KuduClient.KuduClientBuilder kuduClientBuilder = new KuduClient.KuduClientBuilder(kuduMasters); if (null != workerCount) { - asyncKuduClientBuilder.workerCount(workerCount); + kuduClientBuilder.workerCount(workerCount); } if (null != defaultSocketReadTimeoutMs) { - asyncKuduClientBuilder.workerCount(defaultSocketReadTimeoutMs); + kuduClientBuilder.defaultSocketReadTimeoutMs(defaultSocketReadTimeoutMs); } if (null != defaultOperationTimeoutMs) { - asyncKuduClientBuilder.workerCount(defaultOperationTimeoutMs); + kuduClientBuilder.defaultOperationTimeoutMs(defaultOperationTimeoutMs); } if (enableKrb) { @@ -122,20 +113,21 @@ private void establishConnection() throws IOException { krb5conf ); client = ugi.doAs( - new PrivilegedAction() { - @Override - public AsyncKuduClient run() { - return asyncKuduClientBuilder.build(); - } - }); + (PrivilegedAction) kuduClientBuilder::build); } else { - client = asyncKuduClientBuilder.build(); + client = kuduClientBuilder.build(); + } + + if (client.tableExists(tableName)) { + table = client.openTable(tableName); } - LOG.info("connect kudu is successed!"); - KuduClient syncClient = client.syncClient(); - if (syncClient.tableExists(tableName)) { - table = syncClient.openTable(tableName); + if (Objects.isNull(table)) { + throw new IllegalArgumentException( + String.format("Table [%s] Open Failed , please check table exists", tableName)); } + LOG.info("connect kudu is succeed!"); + + session = client.newSession(); } @Override @@ -147,26 +139,22 @@ public void writeRecord(Tuple2 record) throws IOException { } Row row = tupleTrans.getField(1); if (row.getArity() != fieldNames.length) { - if(outDirtyRecords.getCount() % DIRTY_PRINT_FREQUENCY == 0) { + if (outDirtyRecords.getCount() % DIRTY_PRINT_FREQUENCY == 0) { LOG.error("record insert failed ..{}", row.toString()); LOG.error("cause by row.getArity() != fieldNames.length"); } outDirtyRecords.inc(); return; } - Operation operation = toOperation(writeMode, row); - AsyncKuduSession session = client.newSession(); try { if (outRecords.getCount() % ROW_PRINT_FREQUENCY == 0) { LOG.info("Receive data : {}", row); } - - session.apply(operation); - session.close(); + session.apply(toOperation(writeMode, row)); outRecords.inc(); } catch (KuduException e) { - if(outDirtyRecords.getCount() % DIRTY_PRINT_FREQUENCY == 0){ + if (outDirtyRecords.getCount() % DIRTY_PRINT_FREQUENCY == 0) { LOG.error("record insert failed, total dirty record:{} current row:{}", outDirtyRecords.getCount(), row.toString()); LOG.error("", e); } @@ -176,107 +164,24 @@ public void writeRecord(Tuple2 record) throws IOException { @Override public void close() { - if (null != client) { + if (Objects.nonNull(session) && !session.isClosed()) { try { - client.close(); + session.close(); } catch (Exception e) { - throw new IllegalArgumentException("[closeKudu]:" + e.getMessage()); + throw new IllegalArgumentException("[closeKuduSession]: " + e.getMessage()); } } - } - - public static KuduOutputFormatBuilder buildKuduOutputFormat() { - return new KuduOutputFormatBuilder(); - } - - public static class KuduOutputFormatBuilder { - private final KuduOutputFormat kuduOutputFormat; - - protected KuduOutputFormatBuilder() { - this.kuduOutputFormat = new KuduOutputFormat(); - } - - public KuduOutputFormatBuilder setKuduMasters(String kuduMasters) { - kuduOutputFormat.kuduMasters = kuduMasters; - return this; - } - - public KuduOutputFormatBuilder setTableName(String tableName) { - kuduOutputFormat.tableName = tableName; - return this; - } - - - public KuduOutputFormatBuilder setFieldNames(String[] fieldNames) { - kuduOutputFormat.fieldNames = fieldNames; - return this; - } - - public KuduOutputFormatBuilder setFieldTypes(TypeInformation[] fieldTypes) { - kuduOutputFormat.fieldTypes = fieldTypes; - return this; - } - - public KuduOutputFormatBuilder setWriteMode(WriteMode writeMode) { - if (null == writeMode) { - kuduOutputFormat.writeMode = WriteMode.UPSERT; - } - kuduOutputFormat.writeMode = writeMode; - return this; - } - - public KuduOutputFormatBuilder setWorkerCount(Integer workerCount) { - kuduOutputFormat.workerCount = workerCount; - return this; - } - - public KuduOutputFormatBuilder setDefaultOperationTimeoutMs(Integer defaultOperationTimeoutMs) { - kuduOutputFormat.defaultOperationTimeoutMs = defaultOperationTimeoutMs; - return this; - } - - public KuduOutputFormatBuilder setDefaultSocketReadTimeoutMs(Integer defaultSocketReadTimeoutMs) { - kuduOutputFormat.defaultSocketReadTimeoutMs = defaultSocketReadTimeoutMs; - return this; - } - - public KuduOutputFormatBuilder setPrincipal(String principal) { - kuduOutputFormat.principal = principal; - return this; - } - - public KuduOutputFormatBuilder setKeytab(String keytab) { - kuduOutputFormat.keytab = keytab; - return this; - } - - public KuduOutputFormatBuilder setKrb5conf(String krb5conf) { - kuduOutputFormat.krb5conf = krb5conf; - return this; - } - - public KuduOutputFormatBuilder setEnableKrb(boolean enableKrb) { - kuduOutputFormat.enableKrb = enableKrb; - return this; - } - public KuduOutputFormat finish() { - if (kuduOutputFormat.kuduMasters == null) { - throw new IllegalArgumentException("No kuduMasters supplied."); - } - - if (kuduOutputFormat.tableName == null) { - throw new IllegalArgumentException("No tablename supplied."); + if (null != client) { + try { + client.shutdown(); + } catch (Exception e) { + throw new IllegalArgumentException("[closeKuduClient]:" + e.getMessage()); } - - return kuduOutputFormat; } } private Operation toOperation(WriteMode writeMode, Row row) { - if (null == table) { - throw new IllegalArgumentException("Table Open Failed , please check table exists"); - } Operation operation = toOperation(writeMode); PartialRow partialRow = operation.getRow(); @@ -355,11 +260,102 @@ private Operation toOperation(WriteMode writeMode) { return table.newInsert(); case UPDATE: return table.newUpdate(); - case UPSERT: - return table.newUpsert(); default: return table.newUpsert(); } } + public enum WriteMode { + // insert + INSERT, + // update + UPDATE, + // update or insert + UPSERT + } + + public static class KuduOutputFormatBuilder { + private final KuduOutputFormat kuduOutputFormat; + + protected KuduOutputFormatBuilder() { + this.kuduOutputFormat = new KuduOutputFormat(); + } + + public KuduOutputFormatBuilder setKuduMasters(String kuduMasters) { + kuduOutputFormat.kuduMasters = kuduMasters; + return this; + } + + public KuduOutputFormatBuilder setTableName(String tableName) { + kuduOutputFormat.tableName = tableName; + return this; + } + + + public KuduOutputFormatBuilder setFieldNames(String[] fieldNames) { + kuduOutputFormat.fieldNames = fieldNames; + return this; + } + + public KuduOutputFormatBuilder setFieldTypes(TypeInformation[] fieldTypes) { + kuduOutputFormat.fieldTypes = fieldTypes; + return this; + } + + public KuduOutputFormatBuilder setWriteMode(WriteMode writeMode) { + if (null == writeMode) { + kuduOutputFormat.writeMode = WriteMode.UPSERT; + } + kuduOutputFormat.writeMode = writeMode; + return this; + } + + public KuduOutputFormatBuilder setWorkerCount(Integer workerCount) { + kuduOutputFormat.workerCount = workerCount; + return this; + } + + public KuduOutputFormatBuilder setDefaultOperationTimeoutMs(Integer defaultOperationTimeoutMs) { + kuduOutputFormat.defaultOperationTimeoutMs = defaultOperationTimeoutMs; + return this; + } + + public KuduOutputFormatBuilder setDefaultSocketReadTimeoutMs(Integer defaultSocketReadTimeoutMs) { + kuduOutputFormat.defaultSocketReadTimeoutMs = defaultSocketReadTimeoutMs; + return this; + } + + public KuduOutputFormatBuilder setPrincipal(String principal) { + kuduOutputFormat.principal = principal; + return this; + } + + public KuduOutputFormatBuilder setKeytab(String keytab) { + kuduOutputFormat.keytab = keytab; + return this; + } + + public KuduOutputFormatBuilder setKrb5conf(String krb5conf) { + kuduOutputFormat.krb5conf = krb5conf; + return this; + } + + public KuduOutputFormatBuilder setEnableKrb(boolean enableKrb) { + kuduOutputFormat.enableKrb = enableKrb; + return this; + } + + public KuduOutputFormat finish() { + if (kuduOutputFormat.kuduMasters == null) { + throw new IllegalArgumentException("No kuduMasters supplied."); + } + + if (kuduOutputFormat.tableName == null) { + throw new IllegalArgumentException("No tablename supplied."); + } + + return kuduOutputFormat; + } + } + } \ No newline at end of file diff --git a/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduSink.java b/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduSink.java index f8ac125be..cd87e3fbf 100644 --- a/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduSink.java +++ b/kudu/kudu-sink/src/main/java/com/dtstack/flink/sql/sink/kudu/KuduSink.java @@ -20,28 +20,19 @@ public class KuduSink implements RetractStreamTableSink, Serializable, IStreamSinkGener { + protected String[] fieldNames; + TypeInformation[] fieldTypes; + boolean enableKrb; private String kuduMasters; - private String tableName; - private KuduOutputFormat.WriteMode writeMode; - - protected String[] fieldNames; - - TypeInformation[] fieldTypes; - private Integer workerCount; - private Integer defaultOperationTimeoutMs; - private Integer defaultSocketReadTimeoutMs; - private int parallelism = -1; - private String principal; private String keytab; private String krb5conf; - boolean enableKrb; @Override public KuduSink genStreamSink(AbstractTargetTableInfo targetTableInfo) { @@ -67,7 +58,7 @@ public void emitDataStream(DataStream> dataStream) { public DataStreamSink> consumeDataStream(DataStream> dataStream) { KuduOutputFormat.KuduOutputFormatBuilder builder = KuduOutputFormat.buildKuduOutputFormat(); - builder.setKuduMasters(this.kuduMasters) + KuduOutputFormat kuduOutputFormat = builder.setKuduMasters(this.kuduMasters) .setTableName(this.tableName) .setWriteMode(writeMode) .setWorkerCount(this.workerCount) @@ -78,8 +69,8 @@ public DataStreamSink> consumeDataStream(DataStream Date: Wed, 6 Jan 2021 18:51:14 +0800 Subject: [PATCH 088/103] [hotfix-33981][elasticsearch6][sink]remove logback dependence and exclude jar from 'flink-connector-elasticsearch6_2.11' --- elasticsearch6/elasticsearch6-sink/pom.xml | 24 ++++++---------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/elasticsearch6/elasticsearch6-sink/pom.xml b/elasticsearch6/elasticsearch6-sink/pom.xml index cb5fb753c..c1e755b2d 100644 --- a/elasticsearch6/elasticsearch6-sink/pom.xml +++ b/elasticsearch6/elasticsearch6-sink/pom.xml @@ -30,28 +30,16 @@ ${elasticsearch.version} - - ch.qos.logback - logback-core - 1.1.7 - - - - ch.qos.logback - logback-classic - 1.1.7 - - - - org.apache.logging.log4j - log4j-to-slf4j - 2.7 - - org.apache.flink flink-connector-elasticsearch6_2.11 ${flink.version} + + + log4j-to-slf4j + org.apache.logging.log4j + + From 081b0c2a1fcb8e817217312e60649667c59663bc Mon Sep 17 00:00:00 2001 From: wuren Date: Wed, 6 Jan 2021 19:45:55 +0800 Subject: [PATCH 089/103] [fix-34400][root][pom] optimize pom for cleaning output dir --- pom.xml | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index f2e85ea33..1b797690d 100644 --- a/pom.xml +++ b/pom.xml @@ -138,23 +138,19 @@ - maven-antrun-plugin - 1.8 - - - clean-plugins - - clean - - run - - - - - - - - + org.apache.maven.plugins + maven-clean-plugin + 3.1.0 + + + + ${basedir}/plugins + + + ${basedir}/sqlplugins + + + From 4388684ea2c16bf4effd15c214a5ded2a6cc6de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=93=81=E6=9F=B1?= Date: Thu, 7 Jan 2021 17:59:01 +0800 Subject: [PATCH 090/103] =?UTF-8?q?[fix-34473][core]=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=85=A8=E5=B1=80=E5=B9=B6=E8=A1=8C=E5=BA=A6?= =?UTF-8?q?=E5=90=8E=EF=BC=8C=E5=85=A8=E9=87=8F=E7=BB=B4=E8=A1=A8=E5=8D=95?= =?UTF-8?q?=E7=8B=AC=E8=AE=BE=E7=BD=AE=E5=B9=B6=E8=A1=8C=E5=BA=A6=E4=B8=8D?= =?UTF-8?q?=E7=94=9F=E6=95=88=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flink/sql/side/operator/SideWithAllCacheOperator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/side/operator/SideWithAllCacheOperator.java b/core/src/main/java/com/dtstack/flink/sql/side/operator/SideWithAllCacheOperator.java index 6b6f9fe1b..f825180e4 100644 --- a/core/src/main/java/com/dtstack/flink/sql/side/operator/SideWithAllCacheOperator.java +++ b/core/src/main/java/com/dtstack/flink/sql/side/operator/SideWithAllCacheOperator.java @@ -60,6 +60,6 @@ private static BaseAllReqRow loadFlatMap(String sideType, String sqlRootDir, Row public static DataStream getSideJoinDataStream(DataStream inputStream, String sideType, String sqlRootDir, RowTypeInfo rowTypeInfo, JoinInfo joinInfo, List outFieldInfoList, AbstractSideTableInfo sideTableInfo) throws Exception { BaseAllReqRow allReqRow = loadFlatMap(sideType, sqlRootDir, rowTypeInfo, joinInfo, outFieldInfoList, sideTableInfo); - return inputStream.flatMap(allReqRow); + return inputStream.flatMap(allReqRow).setParallelism(sideTableInfo.getParallelism()); } } From b720842135014e93e871326ccfece24afd2d386b Mon Sep 17 00:00:00 2001 From: mowen Date: Fri, 8 Jan 2021 11:27:07 +0800 Subject: [PATCH 091/103] [hotfix-34455][core]verify whether join's columns exists in table. --- .../dtstack/flink/sql/side/SideSqlExec.java | 83 +++++++++++++++++-- 1 file changed, 76 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/side/SideSqlExec.java b/core/src/main/java/com/dtstack/flink/sql/side/SideSqlExec.java index bb8faf8ee..6dad92061 100644 --- a/core/src/main/java/com/dtstack/flink/sql/side/SideSqlExec.java +++ b/core/src/main/java/com/dtstack/flink/sql/side/SideSqlExec.java @@ -60,12 +60,7 @@ import org.slf4j.LoggerFactory; import java.sql.Timestamp; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.Set; +import java.util.*; import static org.apache.calcite.sql.SqlKind.*; @@ -351,7 +346,7 @@ protected void dealAsSourceTable(StreamTableEnvironment tableEnv, FieldReplaceInfo fieldReplaceInfo = parseAsQuery((SqlBasicCall) pollSqlNode, tableCache); if(fieldReplaceInfo == null){ - return; + return; } //as 的源表 @@ -361,6 +356,77 @@ protected void dealAsSourceTable(StreamTableEnvironment tableEnv, } + /** + * check whether all table fields exist in join condition. + * @param conditionNode + * @param joinScope + */ + public void checkConditionFieldsInTable(SqlNode conditionNode, JoinScope joinScope, AbstractSideTableInfo sideTableInfo) { + List sqlNodeList = Lists.newArrayList(); + ParseUtils.parseAnd(conditionNode, sqlNodeList); + for (SqlNode sqlNode : sqlNodeList) { + if (!SqlKind.COMPARISON.contains(sqlNode.getKind())) { + throw new RuntimeException("It is not comparison operator."); + } + + SqlNode leftNode = ((SqlBasicCall) sqlNode).getOperands()[0]; + SqlNode rightNode = ((SqlBasicCall) sqlNode).getOperands()[1]; + + if (leftNode.getKind() == SqlKind.IDENTIFIER) { + checkFieldInTable((SqlIdentifier) leftNode, joinScope, conditionNode, sideTableInfo); + } + + if (rightNode.getKind() == SqlKind.IDENTIFIER) { + checkFieldInTable((SqlIdentifier) rightNode, joinScope, conditionNode, sideTableInfo); + } + + } + } + + /** + * check whether table exists and whether field is in table. + * @param sqlNode + * @param joinScope + * @param conditionNode + */ + private void checkFieldInTable(SqlIdentifier sqlNode, JoinScope joinScope, SqlNode conditionNode, AbstractSideTableInfo sideTableInfo) { + String tableName = sqlNode.getComponent(0).getSimple(); + String fieldName = sqlNode.getComponent(1).getSimple(); + JoinScope.ScopeChild scopeChild = joinScope.getScope(tableName); + String tableErrorMsg = "Table [%s] is not exist. Error condition is [%s]. If you find [%s] is exist. Please check AS statement."; + Preconditions.checkState( + scopeChild != null, + tableErrorMsg, + tableName, + conditionNode.toString(), + tableName + ); + + String[] fieldNames = scopeChild.getRowTypeInfo().getFieldNames(); + ArrayList allFieldNames = new ArrayList( + Arrays.asList(fieldNames) + ); + // HBase、Redis这种NoSQL Primary Key不在字段列表中,所以要加进去。 + if (sideTableInfo != null) { + List pks = sideTableInfo.getPrimaryKeys(); + if (pks != null) { + pks.stream() + .filter(pk -> !allFieldNames.contains(pk)) + .forEach(pk -> allFieldNames.add(pk)); + } + } + + boolean hasField = allFieldNames.contains(fieldName); + String fieldErrorMsg = "Table [%s] has not [%s] field. Error join condition is [%s]. If you find it is exist. Please check AS statement."; + Preconditions.checkState( + hasField, + fieldErrorMsg, + tableName, + fieldName, + conditionNode.toString() + ); + } + private void joinFun(Object pollObj, Map localTableCache, Map sideTableMap, @@ -395,6 +461,9 @@ private void joinFun(Object pollObj, HashBasedTable mappingTable = ((JoinInfo) pollObj).getTableFieldRef(); + // verify whether join's columns exists in table. + checkConditionFieldsInTable(joinInfo.getCondition(), joinScope, sideTableInfo); + //获取两个表的所有字段 List sideJoinFieldInfo = ParserJoinField.getRowTypeInfo(joinInfo.getSelectNode(), joinScope, true); //通过join的查询字段信息过滤出需要的字段信息 From 8009e35325b6ecb42704e98a50f1fe200e4007a8 Mon Sep 17 00:00:00 2001 From: mowen Date: Mon, 18 Jan 2021 15:24:19 +0800 Subject: [PATCH 092/103] [hotfix-34801][core]fix proplem when replace alias and fieldName --- .../com/dtstack/flink/sql/util/TableUtils.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/util/TableUtils.java b/core/src/main/java/com/dtstack/flink/sql/util/TableUtils.java index 30ef8b4b3..9d11014bf 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/TableUtils.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/TableUtils.java @@ -528,11 +528,25 @@ private static void replaceConditionNode(SqlNode selectNode, String oldTbName, S String tableName = sqlIdentifier.names.asList().get(0); String tableField = sqlIdentifier.names.asList().get(1); - String fieldKey = tableName + "_" + tableField; + String fieldKey = tableName + "." + tableField; if(tableName.equalsIgnoreCase(oldTbName)){ - String newFieldName = fieldReplaceRef.get(fieldKey) == null ? tableField : fieldReplaceRef.get(fieldKey); + /* + * ****Before replace:***** + * fieldKey: b.department + * fieldReplaceRef : b.department -> a_b_0.department0 + * oldFieldRef: a_b_0.department0 + * oldTbName: b + * oldFieldName: department + * ****After replace:***** + * newTbName: a_b_0 + * newFieldName: department0 + */ + String oldFieldRef = fieldReplaceRef.get(fieldKey); + String newFieldName = (oldFieldRef != null && !StringUtils.substringAfter(oldFieldRef, ".").isEmpty()) ? + StringUtils.substringAfter(oldFieldRef, ".") : tableField; + SqlIdentifier newField = ((SqlIdentifier)selectNode).setName(0, newTbName); newField = newField.setName(1, newFieldName); ((SqlIdentifier)selectNode).assignNamesFrom(newField); From c36e131dc912ecedb4f099f0ba54e0671b81bd7a Mon Sep 17 00:00:00 2001 From: xiuzhu Date: Tue, 19 Jan 2021 14:03:55 +0800 Subject: [PATCH 093/103] =?UTF-8?q?[hotfix-34751][core]=20udf=20=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E4=BD=BF=E7=94=A8=E5=BD=93=E5=89=8D=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?classloader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flink/sql/classloader/ClassLoaderManager.java | 14 +++++++++++++- .../flink/sql/exec/ExecuteProcessHelper.java | 10 +--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/classloader/ClassLoaderManager.java b/core/src/main/java/com/dtstack/flink/sql/classloader/ClassLoaderManager.java index 2e62e11ab..bd805007e 100644 --- a/core/src/main/java/com/dtstack/flink/sql/classloader/ClassLoaderManager.java +++ b/core/src/main/java/com/dtstack/flink/sql/classloader/ClassLoaderManager.java @@ -20,10 +20,12 @@ import com.dtstack.flink.sql.util.PluginUtil; import com.dtstack.flink.sql.util.ReflectionUtils; +import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.FileInputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; @@ -73,7 +75,17 @@ private static DtClassLoader retrieveClassLoad(String pluginJarPath) { private static DtClassLoader retrieveClassLoad(List jarUrls) { jarUrls.sort(Comparator.comparing(URL::toString)); - String jarUrlkey = StringUtils.join(jarUrls, "_"); + + List jarMd5s = new ArrayList<>(jarUrls.size()); + for (URL jarUrl : jarUrls) { + try (FileInputStream inputStream = new FileInputStream(jarUrl.getPath())){ + String jarMd5 = DigestUtils.md5Hex(inputStream); + jarMd5s.add(jarMd5); + } catch (Exception e) { + throw new RuntimeException("Exceptions appears when read file:" + e); + } + } + String jarUrlkey = StringUtils.join(jarMd5s, "_"); return pluginClassLoader.computeIfAbsent(jarUrlkey, k -> { try { URL[] urls = jarUrls.toArray(new URL[jarUrls.size()]); diff --git a/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java b/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java index d3a6f78be..97c40f93b 100644 --- a/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java +++ b/core/src/main/java/com/dtstack/flink/sql/exec/ExecuteProcessHelper.java @@ -248,18 +248,10 @@ private static void sqlTranslation(String localSqlPluginPath, public static void registerUserDefinedFunction(SqlTree sqlTree, List jarUrlList, TableEnvironment tableEnv, boolean isGetPlan) throws IllegalAccessException, InvocationTargetException { // udf和tableEnv须由同一个类加载器加载 - ClassLoader levelClassLoader = tableEnv.getClass().getClassLoader(); ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); - URLClassLoader classLoader = null; + URLClassLoader classLoader = ClassLoaderManager.loadExtraJar(jarUrlList, (URLClassLoader) currentClassLoader); List funcList = sqlTree.getFunctionList(); for (CreateFuncParser.SqlParserResult funcInfo : funcList) { - if (isGetPlan) { - classLoader = ClassLoaderManager.loadExtraJar(jarUrlList, (URLClassLoader) currentClassLoader); - } - //classloader - if (classLoader == null) { - classLoader = ClassLoaderManager.loadExtraJar(jarUrlList, (URLClassLoader) levelClassLoader); - } FunctionManager.registerUDF(funcInfo.getType(), funcInfo.getClassName(), funcInfo.getName(), tableEnv, classLoader); } } From 9e89588b9a081c053eb24412f04041a3434dad23 Mon Sep 17 00:00:00 2001 From: wuren Date: Thu, 4 Feb 2021 17:17:59 +0800 Subject: [PATCH 094/103] [fix-35290][core] fix watermark metrics throw a warning log --- .../flink/sql/watermarker/AbstractCustomerWaterMarker.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/watermarker/AbstractCustomerWaterMarker.java b/core/src/main/java/com/dtstack/flink/sql/watermarker/AbstractCustomerWaterMarker.java index d75d26a61..9991c7021 100644 --- a/core/src/main/java/com/dtstack/flink/sql/watermarker/AbstractCustomerWaterMarker.java +++ b/core/src/main/java/com/dtstack/flink/sql/watermarker/AbstractCustomerWaterMarker.java @@ -92,7 +92,6 @@ public IterationRuntimeContext getIterationRuntimeContext() { public void setRuntimeContext(RuntimeContext t) { this.runtimeContext = t; eventDelayGauge = new EventDelayGauge(); - t.getMetricGroup().getAllVariables().put("", fromSourceTag); t.getMetricGroup().gauge(MetricConstant.DT_EVENT_DELAY_GAUGE, eventDelayGauge); } From d0b8a1d7afd9d81e407962423bcc13803020cc1f Mon Sep 17 00:00:00 2001 From: tiezhu Date: Thu, 4 Feb 2021 20:51:44 +0800 Subject: [PATCH 095/103] [feat-34267][redis] redis sink and side support ipv6. --- redis5/pom.xml | 13 ++ redis5/redis5-side/pom.xml | 14 --- .../flink/sql/side/redis/RedisAllReqRow.java | 114 ++++++++++-------- .../sql/side/redis/RedisAsyncReqRow.java | 31 +++-- redis5/redis5-side/redis-side-core/pom.xml | 11 +- .../flink/sql/side/redis/enums/RedisType.java | 3 +- .../sql/side/redis/table/RedisSideReqRow.java | 4 +- redis5/redis5-sink/pom.xml | 14 --- .../sql/sink/redis/RedisOutputFormat.java | 64 ++++++---- 9 files changed, 137 insertions(+), 131 deletions(-) diff --git a/redis5/pom.xml b/redis5/pom.xml index 894f786ef..d8e63bef1 100644 --- a/redis5/pom.xml +++ b/redis5/pom.xml @@ -16,5 +16,18 @@ redis5-side + + + com.dtstack.flink + sql.core + 1.0-SNAPSHOT + provided + + + redis.clients + jedis + 2.9.0 + + \ No newline at end of file diff --git a/redis5/redis5-side/pom.xml b/redis5/redis5-side/pom.xml index c6623b9d2..10462574c 100644 --- a/redis5/redis5-side/pom.xml +++ b/redis5/redis5-side/pom.xml @@ -11,20 +11,6 @@ 4.0.0 sql.side.redis redis-side - - - com.dtstack.flink - sql.core - 1.0-SNAPSHOT - provided - - - redis.clients - jedis - 2.8.0 - - - redis-side-core diff --git a/redis5/redis5-side/redis-all-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAllReqRow.java b/redis5/redis5-side/redis-all-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAllReqRow.java index 8e6a08791..2e3c0887b 100644 --- a/redis5/redis5-side/redis-all-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAllReqRow.java +++ b/redis5/redis5-side/redis-all-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAllReqRow.java @@ -26,6 +26,7 @@ import com.dtstack.flink.sql.side.redis.table.RedisSideReqRow; import com.dtstack.flink.sql.side.redis.table.RedisSideTableInfo; import com.esotericsoftware.minlog.Log; +import com.google.common.collect.Maps; import org.apache.calcite.sql.JoinType; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; @@ -35,7 +36,6 @@ import org.apache.flink.table.runtime.types.CRow; import org.apache.flink.types.Row; import org.apache.flink.util.Collector; -import com.google.common.collect.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import redis.clients.jedis.HostAndPort; @@ -48,14 +48,18 @@ import java.io.Closeable; import java.io.IOException; import java.sql.SQLException; - +import java.util.Arrays; import java.util.Calendar; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * @author yanxi */ @@ -63,6 +67,8 @@ public class RedisAllReqRow extends BaseAllReqRow { private static final long serialVersionUID = 7578879189085344807L; + private static final Pattern HOST_PORT_PATTERN = Pattern.compile("(?(.*)):(?\\d+)*"); + private static final Logger LOG = LoggerFactory.getLogger(RedisAllReqRow.class); private static final int CONN_RETRY_NUM = 3; @@ -73,9 +79,9 @@ public class RedisAllReqRow extends BaseAllReqRow { private RedisSideTableInfo tableInfo; - private AtomicReference>> cacheRef = new AtomicReference<>(); + private final AtomicReference>> cacheRef = new AtomicReference<>(); - private RedisSideReqRow redisSideReqRow; + private final RedisSideReqRow redisSideReqRow; public RedisAllReqRow(RowTypeInfo rowTypeInfo, JoinInfo joinInfo, List outFieldInfoList, AbstractSideTableInfo sideTableInfo) { super(new RedisAllSideInfo(rowTypeInfo, joinInfo, outFieldInfoList, sideTableInfo)); @@ -115,8 +121,8 @@ public void flatMap(CRow input, Collector out) throws Exception { for (int i = 0; i < sideInfo.getEqualValIndex().size(); i++) { Integer conValIndex = sideInfo.getEqualValIndex().get(i); Object equalObj = input.row().getField(conValIndex); - if(equalObj == null){ - if(sideInfo.getJoinType() == JoinType.LEFT){ + if (equalObj == null) { + if (sideInfo.getJoinType() == JoinType.LEFT) { Row data = fillData(input.row(), null); out.collect(new CRow(data, input.change())); } @@ -125,13 +131,13 @@ public void flatMap(CRow input, Collector out) throws Exception { inputParams.put(sideInfo.getEqualFieldList().get(i), equalObj.toString()); } String key = buildCacheKey(inputParams); - if(StringUtils.isBlank(key)){ + if (StringUtils.isBlank(key)) { return; } Map cacheMap = cacheRef.get().get(key); - if(MapUtils.isEmpty(cacheMap)){ - if(sideInfo.getJoinType() != JoinType.LEFT){ + if (MapUtils.isEmpty(cacheMap)) { + if (sideInfo.getJoinType() != JoinType.LEFT) { return; } Row data = fillData(input.row(), null); @@ -146,8 +152,8 @@ public void flatMap(CRow input, Collector out) throws Exception { private String buildCacheKey(Map refData) { StringBuilder keyBuilder = new StringBuilder(tableInfo.getTableName()); List primaryKeys = tableInfo.getPrimaryKeys(); - for(String primaryKey : primaryKeys){ - if(!refData.containsKey(primaryKey)){ + for (String primaryKey : primaryKeys) { + if (!refData.containsKey(primaryKey)) { return null; } keyBuilder.append("_").append(refData.get(primaryKey)); @@ -160,7 +166,7 @@ private void loadData(Map> tmpCache) throws SQLExcep JedisCommands jedis = null; try { StringBuilder keyPattern = new StringBuilder(tableInfo.getTableName()); - for (String key : tableInfo.getPrimaryKeys()) { + for (int i = 0; i < tableInfo.getPrimaryKeys().size(); i++) { keyPattern.append("_").append("*"); } jedis = getJedisWithRetry(CONN_RETRY_NUM); @@ -195,41 +201,54 @@ private JedisCommands getJedis(RedisSideTableInfo tableInfo) { String url = tableInfo.getUrl(); String password = tableInfo.getPassword(); String database = tableInfo.getDatabase() == null ? "0" : tableInfo.getDatabase(); + String masterName = tableInfo.getMasterName(); int timeout = tableInfo.getTimeout(); - if (timeout == 0){ - timeout = 1000; + if (timeout == 0) { + timeout = 10000; } String[] nodes = url.split(","); - String[] firstIpPort = nodes[0].split(":"); - String firstIp = firstIpPort[0]; - String firstPort = firstIpPort[1]; - Set addresses = new HashSet<>(); - Set ipPorts = new HashSet<>(); - for (String ipPort : nodes) { - ipPorts.add(ipPort); - String[] ipPortPair = ipPort.split(":"); - addresses.add(new HostAndPort(ipPortPair[0].trim(), Integer.valueOf(ipPortPair[1].trim()))); - } - if (timeout == 0){ - timeout = 1000; - } JedisCommands jedis = null; GenericObjectPoolConfig poolConfig = setPoolConfig(tableInfo.getMaxTotal(), tableInfo.getMaxIdle(), tableInfo.getMinIdle()); - switch (RedisType.parse(tableInfo.getRedisType())){ - //单机 + switch (RedisType.parse(tableInfo.getRedisType())) { case STANDALONE: - pool = new JedisPool(poolConfig, firstIp, Integer.parseInt(firstPort), timeout, password, Integer.parseInt(database)); - jedis = pool.getResource(); + String firstIp = null; + String firstPort = null; + Matcher standalone = HOST_PORT_PATTERN.matcher(nodes[0]); + if (standalone.find()) { + firstIp = standalone.group("host").trim(); + firstPort = standalone.group("port").trim(); + } + if (Objects.nonNull(firstIp)) { + pool = new JedisPool(poolConfig, firstIp, Integer.parseInt(firstPort), timeout, password, Integer.parseInt(database)); + jedis = pool.getResource(); + } else { + throw new IllegalArgumentException( + String.format("redis url error. current url [%s]", nodes[0])); + } break; - //哨兵 case SENTINEL: - jedisSentinelPool = new JedisSentinelPool(tableInfo.getMasterName(), ipPorts, poolConfig, timeout, password, Integer.parseInt(database)); + Set ipPorts = new HashSet<>(Arrays.asList(nodes)); + jedisSentinelPool = new JedisSentinelPool(masterName, ipPorts, poolConfig, timeout, password, Integer.parseInt(database)); jedis = jedisSentinelPool.getResource(); break; - //集群 case CLUSTER: - jedis = new JedisCluster(addresses, timeout, timeout,1, poolConfig); + Set addresses = new HashSet<>(); + // 对ipv6 支持 + for (String node : nodes) { + Matcher matcher = HOST_PORT_PATTERN.matcher(node); + if (matcher.find()) { + String host = matcher.group("host").trim(); + String portStr = matcher.group("port").trim(); + if (org.apache.commons.lang3.StringUtils.isNotBlank(host) && org.apache.commons.lang3.StringUtils.isNotBlank(portStr)) { + // 转化为int格式的端口 + int port = Integer.parseInt(portStr); + addresses.add(new HostAndPort(host, port)); + } + } + } + jedis = new JedisCluster(addresses, timeout, timeout, 10, password, poolConfig); + break; default: break; } @@ -257,35 +276,32 @@ private JedisCommands getJedisWithRetry(int retryNum) { return null; } - private Set getRedisKeys(RedisType redisType, JedisCommands jedis, String keyPattern){ - if(!redisType.equals(RedisType.CLUSTER)){ + private Set getRedisKeys(RedisType redisType, JedisCommands jedis, String keyPattern) { + if (!redisType.equals(RedisType.CLUSTER)) { return ((Jedis) jedis).keys(keyPattern); } Set keys = new TreeSet<>(); - Map clusterNodes = ((JedisCluster)jedis).getClusterNodes(); - for(String k : clusterNodes.keySet()){ + Map clusterNodes = ((JedisCluster) jedis).getClusterNodes(); + for (String k : clusterNodes.keySet()) { JedisPool jp = clusterNodes.get(k); - Jedis connection = jp.getResource(); - try { + try (Jedis connection = jp.getResource()) { keys.addAll(connection.keys(keyPattern)); - } catch (Exception e){ - LOG.error("Getting keys error: {}", e); - } finally { - connection.close(); + } catch (Exception e) { + LOG.error("Getting keys error.", e); } } return keys; } - private GenericObjectPoolConfig setPoolConfig(String maxTotal, String maxIdle, String minIdle){ + private GenericObjectPoolConfig setPoolConfig(String maxTotal, String maxIdle, String minIdle) { GenericObjectPoolConfig config = new GenericObjectPoolConfig(); - if (maxTotal != null){ + if (maxTotal != null) { config.setMaxTotal(Integer.parseInt(maxTotal)); } - if (maxIdle != null){ + if (maxIdle != null) { config.setMaxIdle(Integer.parseInt(maxIdle)); } - if (minIdle != null){ + if (minIdle != null) { config.setMinIdle(Integer.parseInt(minIdle)); } return config; diff --git a/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java b/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java index 954469049..534b3d291 100644 --- a/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java +++ b/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java @@ -73,7 +73,7 @@ public class RedisAsyncReqRow extends BaseAsyncReqRow { private RedisSideTableInfo redisSideTableInfo; - private RedisSideReqRow redisSideReqRow; + private final RedisSideReqRow redisSideReqRow; public RedisAsyncReqRow(RowTypeInfo rowTypeInfo, JoinInfo joinInfo, List outFieldInfoList, AbstractSideTableInfo sideTableInfo) { super(new RedisAsyncSideInfo(rowTypeInfo, joinInfo, outFieldInfoList, sideTableInfo)); @@ -98,7 +98,7 @@ private void buildRedisClient(RedisSideTableInfo tableInfo){ case STANDALONE: RedisURI redisURI = RedisURI.create("redis://" + url); redisURI.setPassword(password); - redisURI.setDatabase(Integer.valueOf(database)); + redisURI.setDatabase(Integer.parseInt(database)); redisClient = RedisClient.create(redisURI); connection = redisClient.connect(); async = connection.async(); @@ -106,7 +106,7 @@ private void buildRedisClient(RedisSideTableInfo tableInfo){ case SENTINEL: RedisURI redisSentinelURI = RedisURI.create("redis-sentinel://" + url); redisSentinelURI.setPassword(password); - redisSentinelURI.setDatabase(Integer.valueOf(database)); + redisSentinelURI.setDatabase(Integer.parseInt(database)); redisSentinelURI.setSentinelMasterId(redisSideTableInfo.getMasterName()); redisClient = RedisClient.create(redisSentinelURI); connection = redisClient.connect(); @@ -135,21 +135,18 @@ public void handleAsyncInvoke(Map inputParams, CRow input, Resul return; } RedisFuture> future = ((RedisHashAsyncCommands) async).hgetall(key); - future.thenAccept(new Consumer>() { - @Override - public void accept(Map values) { - if (MapUtils.isNotEmpty(values)) { - try { - Row row = fillData(input.row(), values); - dealCacheData(key,CacheObj.buildCacheObj(ECacheContentType.SingleLine, values)); - resultFuture.complete(Collections.singleton(new CRow(row, input.change()))); - } catch (Exception e) { - dealFillDataError(input, resultFuture, e); - } - } else { - dealMissKey(input, resultFuture); - dealCacheData(key,CacheMissVal.getMissKeyObj()); + future.thenAccept(values -> { + if (MapUtils.isNotEmpty(values)) { + try { + Row row = fillData(input.row(), values); + dealCacheData(key,CacheObj.buildCacheObj(ECacheContentType.SingleLine, values)); + resultFuture.complete(Collections.singleton(new CRow(row, input.change()))); + } catch (Exception e) { + dealFillDataError(input, resultFuture, e); } + } else { + dealMissKey(input, resultFuture); + dealCacheData(key,CacheMissVal.getMissKeyObj()); } }); } diff --git a/redis5/redis5-side/redis-side-core/pom.xml b/redis5/redis5-side/redis-side-core/pom.xml index 7fb45a291..88e0a00dd 100644 --- a/redis5/redis5-side/redis-side-core/pom.xml +++ b/redis5/redis5-side/redis-side-core/pom.xml @@ -12,15 +12,6 @@ sql.side.redis.core - - - com.dtstack.flink - sql.core - 1.0-SNAPSHOT - provided - - - jar - + jar \ No newline at end of file diff --git a/redis5/redis5-side/redis-side-core/src/main/java/com/dtstack/flink/sql/side/redis/enums/RedisType.java b/redis5/redis5-side/redis-side-core/src/main/java/com/dtstack/flink/sql/side/redis/enums/RedisType.java index 048c32b08..614447a5d 100644 --- a/redis5/redis5-side/redis-side-core/src/main/java/com/dtstack/flink/sql/side/redis/enums/RedisType.java +++ b/redis5/redis5-side/redis-side-core/src/main/java/com/dtstack/flink/sql/side/redis/enums/RedisType.java @@ -28,6 +28,7 @@ public static RedisType parse(int redisType){ return type; } } - throw new RuntimeException("unsupport redis type["+ redisType + "]"); + throw new IllegalArgumentException( + "unsupported redis type["+ redisType + "]"); } } diff --git a/redis5/redis5-side/redis-side-core/src/main/java/com/dtstack/flink/sql/side/redis/table/RedisSideReqRow.java b/redis5/redis5-side/redis-side-core/src/main/java/com/dtstack/flink/sql/side/redis/table/RedisSideReqRow.java index 634cc66fa..af1a1f2e4 100644 --- a/redis5/redis5-side/redis-side-core/src/main/java/com/dtstack/flink/sql/side/redis/table/RedisSideReqRow.java +++ b/redis5/redis5-side/redis-side-core/src/main/java/com/dtstack/flink/sql/side/redis/table/RedisSideReqRow.java @@ -43,7 +43,7 @@ public class RedisSideReqRow implements ISideReqRow, Serializable { private static final TimeZone LOCAL_TZ = TimeZone.getDefault(); - private BaseSideInfo sideInfo; + private final BaseSideInfo sideInfo; public RedisSideReqRow(BaseSideInfo sideInfo){ this.sideInfo = sideInfo; @@ -102,7 +102,7 @@ public void setRowField(Row row, Integer index, BaseSideInfo sideInfo, String va row.setField(index, Float.valueOf(value)); break; case "java.math.BigDecimal": - row.setField(index, BigDecimal.valueOf(Long.valueOf(value))); + row.setField(index, BigDecimal.valueOf(Long.parseLong(value))); break; case "java.sql.Timestamp": row.setField(index, Timestamp.valueOf(value)); diff --git a/redis5/redis5-sink/pom.xml b/redis5/redis5-sink/pom.xml index be6a445ed..4fe35fb09 100644 --- a/redis5/redis5-sink/pom.xml +++ b/redis5/redis5-sink/pom.xml @@ -16,20 +16,6 @@ redis-sink http://maven.apache.org - - - com.dtstack.flink - sql.core - 1.0-SNAPSHOT - provided - - - redis.clients - jedis - 2.9.0 - - - diff --git a/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/RedisOutputFormat.java b/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/RedisOutputFormat.java index ab97cf60e..d86f90685 100644 --- a/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/RedisOutputFormat.java +++ b/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/RedisOutputFormat.java @@ -38,13 +38,17 @@ import java.io.Closeable; import java.io.IOException; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * @author yanxi */ -public class RedisOutputFormat extends AbstractDtRichOutputFormat { +public class RedisOutputFormat extends AbstractDtRichOutputFormat> { private static final Logger LOG = LoggerFactory.getLogger(RedisOutputFormat.class); + private static final Pattern HOST_PORT_PATTERN = Pattern.compile("(?(.*)):(?\\d+)*"); + private String url; private String database = "0"; @@ -77,8 +81,6 @@ public class RedisOutputFormat extends AbstractDtRichOutputFormat { private JedisSentinelPool jedisSentinelPool; - private GenericObjectPoolConfig poolConfig; - private RedisOutputFormat(){ } @Override @@ -107,44 +109,60 @@ private GenericObjectPoolConfig setPoolConfig(String maxTotal, String maxIdle, S } private void establishConnection() { - poolConfig = setPoolConfig(maxTotal, maxIdle, minIdle); + GenericObjectPoolConfig poolConfig = setPoolConfig(maxTotal, maxIdle, minIdle); String[] nodes = StringUtils.split(url, ","); - String[] firstIpPort = StringUtils.split(nodes[0], ":"); - String firstIp = firstIpPort[0]; - String firstPort = firstIpPort[1]; - Set addresses = new HashSet<>(); - Set ipPorts = new HashSet<>(); - for (String ipPort : nodes) { - ipPorts.add(ipPort); - String[] ipPortPair = StringUtils.split(ipPort, ":"); - addresses.add(new HostAndPort(ipPortPair[0].trim(), Integer.valueOf(ipPortPair[1].trim()))); - } switch (RedisType.parse(redisType)){ case STANDALONE: - pool = new JedisPool(poolConfig, firstIp, Integer.parseInt(firstPort), timeout, password, Integer.parseInt(database)); - jedis = pool.getResource(); + String firstIp = null; + String firstPort = null; + Matcher standalone = HOST_PORT_PATTERN.matcher(nodes[0]); + if (standalone.find()) { + firstIp = standalone.group("host").trim(); + firstPort = standalone.group("port").trim(); + } + if (Objects.nonNull(firstIp)) { + pool = new JedisPool(poolConfig, firstIp, Integer.parseInt(firstPort), timeout, password, Integer.parseInt(database)); + jedis = pool.getResource(); + } else { + throw new IllegalArgumentException( + String.format("redis url error. current url [%s]", nodes[0])); + } break; case SENTINEL: + Set ipPorts = new HashSet<>(Arrays.asList(nodes)); jedisSentinelPool = new JedisSentinelPool(masterName, ipPorts, poolConfig, timeout, password, Integer.parseInt(database)); jedis = jedisSentinelPool.getResource(); break; case CLUSTER: + Set addresses = new HashSet<>(); + // 对ipv6 支持 + for (String node : nodes) { + Matcher matcher = HOST_PORT_PATTERN.matcher(node); + if (matcher.find()) { + String host = matcher.group("host").trim(); + String portStr = matcher.group("port").trim(); + if (StringUtils.isNotBlank(host) && StringUtils.isNotBlank(portStr)) { + // 转化为int格式的端口 + int port = Integer.parseInt(portStr); + addresses.add(new HostAndPort(host, port)); + } + } + } jedis = new JedisCluster(addresses, timeout, timeout, 10, password, poolConfig); break; default: - throw new RuntimeException("unsupport redis type[ " + redisType + "]"); + throw new IllegalArgumentException("unsupported redis type[ " + redisType + "]"); } } @Override - public void writeRecord(Tuple2 record) throws IOException { - Tuple2 tupleTrans = record; - Boolean retract = tupleTrans.getField(0); + public void writeRecord(Tuple2 record) throws IOException { + Boolean retract = record.getField(0); if (!retract) { return; } - Row row = tupleTrans.getField(1); + Row row = record.getField(1); if (row.getArity() != fieldNames.length) { return; } @@ -153,9 +171,7 @@ public void writeRecord(Tuple2 record) throws IOException { refData.put(fieldNames[i], row.getField(i)); } String redisKey = buildCacheKey(refData); - refData.entrySet().forEach(e ->{ - jedis.hset(redisKey, e.getKey(), String.valueOf(e.getValue())); - }); + refData.forEach((key, value) -> jedis.hset(redisKey, key, String.valueOf(value))); if (outRecords.getCount() % ROW_PRINT_FREQUENCY == 0){ LOG.info(record.toString()); From 28fc9264413ee876ea4c6747ddf2f2bff9b9a611 Mon Sep 17 00:00:00 2001 From: tiezhu Date: Thu, 4 Feb 2021 21:23:10 +0800 Subject: [PATCH 096/103] [feat-34267][redis] fix multi-primary-key with whitespace throw null when buildCache key. multi-primary-key like 'PRIMARY KEY(id, name, ....)' --- .../flink/sql/table/AbstractTableParser.java | 26 +++++++++++-------- .../sql/sink/redis/table/RedisSinkParser.java | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableParser.java b/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableParser.java index f0898cfd6..3df278d5e 100644 --- a/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableParser.java +++ b/core/src/main/java/com/dtstack/flink/sql/table/AbstractTableParser.java @@ -26,10 +26,12 @@ import org.apache.commons.lang3.StringUtils; import org.apache.flink.api.java.tuple.Tuple2; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Reason: @@ -44,16 +46,16 @@ public abstract class AbstractTableParser { private static final String NEST_JSON_FIELD_KEY = "nestFieldKey"; private static final String CHAR_TYPE_NO_LENGTH = "CHAR"; - private static Pattern primaryKeyPattern = Pattern.compile("(?i)PRIMARY\\s+KEY\\s*\\((.*)\\)"); - private static Pattern nestJsonFieldKeyPattern = Pattern.compile("(?i)((@*\\S+\\.)*\\S+)\\s+(.+?)\\s+AS\\s+(\\w+)(\\s+NOT\\s+NULL)?$"); - private static Pattern physicalFieldFunPattern = Pattern.compile("\\w+\\((\\w+)\\)$"); - private static Pattern charTypePattern = Pattern.compile("(?i)CHAR\\((\\d*)\\)$"); - private static Pattern typePattern = Pattern.compile("(\\S+)\\s+(\\w+.*)"); + private static final Pattern primaryKeyPattern = Pattern.compile("(?i)(^\\s*)PRIMARY\\s+KEY\\s*\\((.*)\\)"); + private static final Pattern nestJsonFieldKeyPattern = Pattern.compile("(?i)((@*\\S+\\.)*\\S+)\\s+(.+?)\\s+AS\\s+(\\w+)(\\s+NOT\\s+NULL)?$"); + private static final Pattern physicalFieldFunPattern = Pattern.compile("\\w+\\((\\w+)\\)$"); + private static final Pattern charTypePattern = Pattern.compile("(?i)CHAR\\((\\d*)\\)$"); + private static final Pattern typePattern = Pattern.compile("(\\S+)\\s+(\\w+.*)"); - private Map patternMap = Maps.newHashMap(); + private final Map patternMap = Maps.newHashMap(); - private Map handlerMap = Maps.newHashMap(); + private final Map handlerMap = Maps.newHashMap(); public AbstractTableParser() { addParserHandler(PRIMARY_KEY, primaryKeyPattern, this::dealPrimaryKey); @@ -112,7 +114,7 @@ public void parseFieldsInfo(String fieldsInfo, AbstractTableInfo tableInfo) { if (matcher.find()) { fieldClass = dbTypeConvertToJavaType(CHAR_TYPE_NO_LENGTH); fieldExtraInfo = new AbstractTableInfo.FieldExtraInfo(); - fieldExtraInfo.setLength(Integer.valueOf(matcher.group(1))); + fieldExtraInfo.setLength(Integer.parseInt(matcher.group(1))); } else { fieldClass = dbTypeConvertToJavaType(fieldType); } @@ -140,9 +142,11 @@ private Tuple2 extractType(String fieldRow, String tableName) { } public void dealPrimaryKey(Matcher matcher, AbstractTableInfo tableInfo) { - String primaryFields = matcher.group(1).trim(); - String[] splitArray = primaryFields.split(","); - List primaryKeys = Lists.newArrayList(splitArray); + String primaryFields = matcher.group(2).trim(); + List primaryKeys = Arrays + .stream(primaryFields.split(",")) + .map(String::trim) + .collect(Collectors.toList()); tableInfo.setPrimaryKeys(primaryKeys); } diff --git a/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/table/RedisSinkParser.java b/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/table/RedisSinkParser.java index 8961f7da9..44d127884 100644 --- a/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/table/RedisSinkParser.java +++ b/redis5/redis5-sink/src/main/java/com/dtstack/flink/sql/sink/redis/table/RedisSinkParser.java @@ -51,7 +51,7 @@ public AbstractTableInfo getTableInfo(String tableName, String fieldsInfo, Map primaryKeysList = Lists.newArrayList(); + List primaryKeysList; primaryKeysList = Arrays.asList(StringUtils.split(primaryKeysStr, ",")); redisTableInfo.setPrimaryKeys(primaryKeysList); } From 3b598af38c5b3ecb9d021c597d554b8ca4e4e093 Mon Sep 17 00:00:00 2001 From: tiezhu Date: Sun, 7 Feb 2021 19:57:13 +0800 Subject: [PATCH 097/103] [feat-35355][redis] fix redis async side plugin connect error on sentinel mode. --- .../sql/side/redis/RedisAsyncReqRow.java | 68 ++++++++++++++----- .../sql/side/redis/table/RedisSideReqRow.java | 3 + 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java b/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java index 954469049..3dd39e759 100644 --- a/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java +++ b/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java @@ -18,20 +18,9 @@ package com.dtstack.flink.sql.side.redis; +import com.dtstack.flink.sql.enums.ECacheContentType; import com.dtstack.flink.sql.side.AbstractSideTableInfo; import com.dtstack.flink.sql.side.BaseAsyncReqRow; -import io.lettuce.core.KeyValue; -import io.lettuce.core.RedisURI; -import io.lettuce.core.api.async.RedisAsyncCommands; -import io.lettuce.core.api.async.RedisStringAsyncCommands; -import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; -import org.apache.flink.api.java.typeutils.RowTypeInfo; -import org.apache.flink.configuration.Configuration; -import org.apache.flink.streaming.api.functions.async.ResultFuture; -import org.apache.flink.table.runtime.types.CRow; -import org.apache.flink.types.Row; - -import com.dtstack.flink.sql.enums.ECacheContentType; import com.dtstack.flink.sql.side.CacheMissVal; import com.dtstack.flink.sql.side.FieldInfo; import com.dtstack.flink.sql.side.JoinInfo; @@ -41,6 +30,7 @@ import com.dtstack.flink.sql.side.redis.table.RedisSideTableInfo; import io.lettuce.core.RedisClient; import io.lettuce.core.RedisFuture; +import io.lettuce.core.RedisURI; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.async.RedisHashAsyncCommands; import io.lettuce.core.api.async.RedisKeyAsyncCommands; @@ -48,12 +38,19 @@ import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang.StringUtils; -import com.google.common.collect.Maps; +import org.apache.flink.api.java.typeutils.RowTypeInfo; +import org.apache.flink.configuration.Configuration; +import org.apache.flink.streaming.api.functions.async.ResultFuture; +import org.apache.flink.table.runtime.types.CRow; +import org.apache.flink.types.Row; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Consumer; +import java.util.regex.Matcher; + /** * @author yanxi */ @@ -104,11 +101,34 @@ private void buildRedisClient(RedisSideTableInfo tableInfo){ async = connection.async(); break; case SENTINEL: - RedisURI redisSentinelURI = RedisURI.create("redis-sentinel://" + url); - redisSentinelURI.setPassword(password); - redisSentinelURI.setDatabase(Integer.valueOf(database)); - redisSentinelURI.setSentinelMasterId(redisSideTableInfo.getMasterName()); - redisClient = RedisClient.create(redisSentinelURI); + String[] urlSplit = StringUtils.split(url, ","); + RedisURI.Builder builder = null; + for (String item : urlSplit) { + Matcher mather = RedisSideReqRow.HOST_PORT_PATTERN.matcher(item); + if (mather.find()) { + builder = buildSentinelUri( + mather.group("host"), + mather.group("port"), + builder + ); + } else { + throw new IllegalArgumentException( + String.format("Illegal format with redis url [%s]", item) + ); + } + } + + if (Objects.nonNull(builder)) { + builder + .withPassword(tableInfo.getPassword()) + .withDatabase(Integer.parseInt(tableInfo.getDatabase())) + .withSentinelMasterId(tableInfo.getMasterName()); + } else { + throw new NullPointerException("build redis uri error!"); + } + + RedisURI uri = builder.build(); + redisClient = RedisClient.create(uri); connection = redisClient.connect(); async = connection.async(); break; @@ -123,6 +143,18 @@ private void buildRedisClient(RedisSideTableInfo tableInfo){ } } + private RedisURI.Builder buildSentinelUri( + String host, + String port, + RedisURI.Builder builder) { + if (Objects.nonNull(builder)) { + builder.withSentinel(host, Integer.parseInt(port)); + } else { + builder = RedisURI.Builder.sentinel(host, Integer.parseInt(port)); + } + return builder; + } + @Override public Row fillData(Row input, Object sideInput) { return redisSideReqRow.fillData(input, sideInput); diff --git a/redis5/redis5-side/redis-side-core/src/main/java/com/dtstack/flink/sql/side/redis/table/RedisSideReqRow.java b/redis5/redis5-side/redis-side-core/src/main/java/com/dtstack/flink/sql/side/redis/table/RedisSideReqRow.java index 634cc66fa..a51be6543 100644 --- a/redis5/redis5-side/redis-side-core/src/main/java/com/dtstack/flink/sql/side/redis/table/RedisSideReqRow.java +++ b/redis5/redis5-side/redis-side-core/src/main/java/com/dtstack/flink/sql/side/redis/table/RedisSideReqRow.java @@ -29,6 +29,7 @@ import java.sql.Timestamp; import java.util.Map; import java.util.TimeZone; +import java.util.regex.Pattern; /** * redis fill row data @@ -41,6 +42,8 @@ public class RedisSideReqRow implements ISideReqRow, Serializable { private static final long serialVersionUID = 3751171828444748982L; + public static final Pattern HOST_PORT_PATTERN = Pattern.compile("(?(.*)):(?\\d+)*"); + private static final TimeZone LOCAL_TZ = TimeZone.getDefault(); private BaseSideInfo sideInfo; From c06b190bf6a261c009d7f9ca13cbc41bd73938e2 Mon Sep 17 00:00:00 2001 From: mowen Date: Fri, 5 Mar 2021 11:18:57 +0800 Subject: [PATCH 098/103] [hotfix-35517][elasticsearch5&6][sink]log framework has been replaced from logback to log4j. --- elasticsearch5/elasticsearch5-sink/pom.xml | 19 +++++++------------ elasticsearch6/elasticsearch6-sink/pom.xml | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/elasticsearch5/elasticsearch5-sink/pom.xml b/elasticsearch5/elasticsearch5-sink/pom.xml index 515410c6e..0ad18338a 100644 --- a/elasticsearch5/elasticsearch5-sink/pom.xml +++ b/elasticsearch5/elasticsearch5-sink/pom.xml @@ -19,24 +19,19 @@ 5.3.3 - - ch.qos.logback - logback-core - 1.1.7 - - - - ch.qos.logback - logback-classic - 1.1.7 - - org.elasticsearch.client x-pack-transport 5.3.3 + + + org.slf4j + slf4j-log4j12 + 1.6.1 + + org.apache.logging.log4j log4j-to-slf4j diff --git a/elasticsearch6/elasticsearch6-sink/pom.xml b/elasticsearch6/elasticsearch6-sink/pom.xml index c1e755b2d..8caa7eeb8 100644 --- a/elasticsearch6/elasticsearch6-sink/pom.xml +++ b/elasticsearch6/elasticsearch6-sink/pom.xml @@ -30,6 +30,25 @@ ${elasticsearch.version} + + log4j + log4j + 1.2.17 + + + + slf4j-api + org.slf4j + 1.7.15 + + + + + org.slf4j + slf4j-log4j12 + 1.6.1 + + org.apache.flink flink-connector-elasticsearch6_2.11 From 79692cc7f1e0b1cd283374073b9d2f4a424b72a9 Mon Sep 17 00:00:00 2001 From: tiezhu Date: Thu, 4 Mar 2021 19:56:12 +0800 Subject: [PATCH 099/103] =?UTF-8?q?[hotfix-35512][impala]=201.=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=89=B9=E9=87=8F=E5=86=99=E5=85=A5=E6=97=B6=E5=87=BA?= =?UTF-8?q?=E7=8E=B0=E5=BC=82=E5=B8=B8=EF=BC=8C=E5=B0=86=E6=89=B9=E9=87=8F?= =?UTF-8?q?sql=E8=BD=AC=E4=B8=BA=E5=8D=95=E6=9D=A1sql=E6=89=A7=E8=A1=8C=20?= =?UTF-8?q?2.fix=20inserting=20the=20value=20like=20'[a,b]'=20or=20'{a,=20?= =?UTF-8?q?b}'=20or=20'=20'=20error=20with=20impala=20sink.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flink/sql/exception/ExceptionTrace.java | 39 +++ .../dtstack/flink/sql/util/DtStringUtil.java | 22 +- .../sql/sink/impala/ImpalaOutputFormat.java | 263 +++++++++--------- 3 files changed, 183 insertions(+), 141 deletions(-) create mode 100644 core/src/main/java/com/dtstack/flink/sql/exception/ExceptionTrace.java diff --git a/core/src/main/java/com/dtstack/flink/sql/exception/ExceptionTrace.java b/core/src/main/java/com/dtstack/flink/sql/exception/ExceptionTrace.java new file mode 100644 index 000000000..07db131d9 --- /dev/null +++ b/core/src/main/java/com/dtstack/flink/sql/exception/ExceptionTrace.java @@ -0,0 +1,39 @@ +/* + * 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.dtstack.flink.sql.exception; + +import java.util.Objects; + +/** + * @author tiezhu + * @date 2021/3/8 星期一 + * Company dtstack + */ +public class ExceptionTrace { + // 追溯当前异常的最原始异常信息 + public static String traceOriginalCause(Throwable e) { + String errorMsg; + if (Objects.nonNull(e.getCause())) { + errorMsg = traceOriginalCause(e.getCause()); + } else { + errorMsg = e.getMessage(); + } + return errorMsg; + } +} diff --git a/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java b/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java index 5af657512..dbf6d9e7b 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java @@ -21,15 +21,17 @@ package com.dtstack.flink.sql.util; import com.dtstack.flink.sql.enums.ColumnType; -import org.apache.commons.lang3.StringUtils; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Strings; import com.google.common.collect.Maps; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.sql.Timestamp; +import org.apache.commons.lang3.StringUtils; + import java.math.BigDecimal; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -204,7 +206,6 @@ public static String dealSqlComment(String sql) { return b.toString(); } - public static String col2string(Object column, String type) { String rowData = column.toString(); ColumnType columnType = ColumnType.valueOf(type.toUpperCase()); @@ -374,8 +375,7 @@ public static String getTableFullPath(String schema, String tableName) { return addQuoteForStr(tableName); } - String schemaAndTabName = addQuoteForStr(schema) + "." + addQuoteForStr(tableName); - return schemaAndTabName; + return addQuoteForStr(schema) + "." + addQuoteForStr(tableName); } /** @@ -413,4 +413,14 @@ public static String removeStartAndEndQuota(String str) { String removeStart = StringUtils.removeStart(str, "'"); return StringUtils.removeEnd(removeStart, "'"); } + + /** + * 判断当前对象是null 还是空 + * + * @param obj 需要判断的对象 + * @return 返回true 如果对象是空或者为null + */ + public static boolean isEmptyOrNull(Object obj) { + return Objects.isNull(obj) || obj.toString().isEmpty(); + } } diff --git a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java index 35b0c9a50..82122e00f 100644 --- a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java +++ b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java @@ -18,10 +18,12 @@ package com.dtstack.flink.sql.sink.impala; +import com.dtstack.flink.sql.exception.ExceptionTrace; import com.dtstack.flink.sql.factory.DTThreadFactory; import com.dtstack.flink.sql.outputformat.AbstractDtRichOutputFormat; import com.dtstack.flink.sql.sink.rdb.JDBCTypeConvertUtils; import com.dtstack.flink.sql.table.AbstractTableInfo; +import com.dtstack.flink.sql.util.DtStringUtil; import com.dtstack.flink.sql.util.JDBCUtils; import com.dtstack.flink.sql.util.KrbUtils; import com.google.common.collect.Maps; @@ -41,7 +43,6 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -72,8 +73,6 @@ public class ImpalaOutputFormat extends AbstractDtRichOutputFormat cast('value' as string) cast(value as timestamp) -> cast('value' as timestamp) - private static final Pattern TYPE_PATTERN = Pattern.compile("cast\\((.*) as (.*)\\)"); //specific type which values need to be quoted private static final String[] NEED_QUOTE_TYPE = {"string", "timestamp", "varchar"}; @@ -178,27 +177,20 @@ private void init() throws SQLException { if (!storeType.equalsIgnoreCase(KUDU_TYPE)) { throw new IllegalArgumentException("update mode not support for non-kudu table!"); } - updateStatement = connection.prepareStatement(buildUpdateSql(schema, tableName, fieldNames, primaryKeys)); - return; + } else { + valueFieldNames = rebuildFieldNameListAndTypeList(fieldNames, staticPartitionFields, fieldTypes, partitionFields); } - - valueFieldNames = rebuildFieldNameListAndTypeList(fieldNames, staticPartitionFields, fieldTypes, partitionFields); } private void initScheduledTask(Long batchWaitInterval) { try { if (batchWaitInterval != 0) { this.scheduler = new ScheduledThreadPoolExecutor(1, - new DTThreadFactory("impala-upsert-output-format")); + new DTThreadFactory("impala-upsert-output-format")); this.scheduledFuture = this.scheduler.scheduleWithFixedDelay(() -> { synchronized (ImpalaOutputFormat.this) { - try { - flush(); - } catch (Exception e) { - LOG.error("Writing records to impala jdbc failed.", e); - throw new RuntimeException("Writing records to impala jdbc failed.", e); - } + flush(); } }, batchWaitInterval, batchWaitInterval, TimeUnit.MILLISECONDS); } @@ -237,16 +229,16 @@ private void openJdbc() { } } - private void flush() throws Exception { - if (batchCount > 0) { - if (updateMode.equalsIgnoreCase(UPDATE_MODE)) { - executeUpdateBatch(); - } - if (!rowDataMap.isEmpty()) { - String templateSql = + private synchronized void flush() { + try { + if (batchCount > 0) { + if (updateMode.equalsIgnoreCase(UPDATE_MODE)) { + executeUpdateBatch(); + } + if (!rowDataMap.isEmpty()) { + String templateSql = "INSERT INTO tableName ${tableFieldsCondition} PARTITION ${partitionCondition} VALUES ${valuesCondition}"; - executeBatchSql( - statement, + executeBatchSql( templateSql, schema, tableName, @@ -255,12 +247,15 @@ private void flush() throws Exception { valueFieldNames, partitionFields, rowDataMap - ); - rowDataMap.clear(); + ); + rowDataMap.clear(); + } } + batchCount = 0; + } catch (Exception e) { + LOG.error("Writing records to impala jdbc failed.", e); + throw new RuntimeException("Writing records to impala jdbc failed.", e); } - batchCount = 0; - } /** @@ -273,10 +268,10 @@ private void executeUpdateBatch() throws SQLException { rows.forEach(row -> { try { JDBCTypeConvertUtils.setRecordToStatement( - updateStatement, - JDBCTypeConvertUtils.getSqlTypeFromFieldType(fieldTypes), - row, - primaryKeys.stream().mapToInt(fieldNames::indexOf).toArray() + updateStatement, + JDBCTypeConvertUtils.getSqlTypeFromFieldType(fieldTypes), + row, + primaryKeys.stream().mapToInt(fieldNames::indexOf).toArray() ); updateStatement.addBatch(); } catch (Exception e) { @@ -288,8 +283,8 @@ private void executeUpdateBatch() throws SQLException { rows.clear(); } catch (Exception e) { LOG.debug("impala jdbc execute batch error ", e); - connection.rollback(); - connection.commit(); + JDBCUtils.rollBack(connection); + JDBCUtils.commit(connection); updateStatement.clearBatch(); executeUpdate(connection); } @@ -300,14 +295,10 @@ public void executeUpdate(Connection connection) { try { setRecordToStatement(updateStatement, JDBCTypeConvertUtils.getSqlTypeFromFieldType(fieldTypes), row); updateStatement.executeUpdate(); - connection.commit(); + JDBCUtils.commit(connection); } catch (Exception e) { - try { - connection.rollback(); - connection.commit(); - } catch (SQLException e1) { - throw new RuntimeException(e1); - } + JDBCUtils.rollBack(connection); + JDBCUtils.commit(connection); if (metricOutputFormat.outDirtyRecords.getCount() % DIRTY_DATA_PRINT_FREQUENCY == 0 || LOG.isDebugEnabled()) { LOG.error("record insert failed ,this row is {}", row.toString()); LOG.error("", e); @@ -331,7 +322,7 @@ private void putRowIntoMap(Map> rowDataMap, Tuple2 rebuildFieldNameListAndTypeList(List fieldNames, List staticPartitionFields, List fieldTypes, String partitionFields) { - if (partitionFields.isEmpty()) { + if (partitionFields == null || partitionFields.isEmpty()) { return fieldNames; } @@ -354,40 +345,6 @@ private List rebuildFieldNameListAndTypeList(List fieldNames, Li return valueFields; } - /** - * Quote a specific type of value, like string, timestamp - * before: 1, cast(tiezhu as string), cast(2001-01-09 01:05:01 as timestamp), cast(123 as int) - * after: 1, cast('tiezhu' as string), cast('2001-01-09 01:05:01' as timestamp), cast(123 as int) - * if cast value is null, then cast(null as type) - * - * @param valueCondition original value condition - * @return quoted condition - */ - private String valueConditionAddQuotation(String valueCondition) { - String[] temps = valueCondition.split(","); - List replacedItem = new ArrayList<>(); - Arrays.stream(temps).forEach( - item -> { - Matcher matcher = TYPE_PATTERN.matcher(item); - while (matcher.find()) { - String value = matcher.group(1); - String type = matcher.group(2); - - for (String needQuoteType : NEED_QUOTE_TYPE) { - if (type.contains(needQuoteType)) { - if (!"null".equals(value)) { - item = item.replace(value, "'" + value + "'"); - } - } - } - } - replacedItem.add(item); - } - ); - - return "(" + String.join(", ", replacedItem) + ")"; - } - @Override public void writeRecord(Tuple2 record) throws IOException { try { @@ -422,7 +379,7 @@ public void writeRecord(Tuple2 record) throws IOException { for (int i = 0; i < fieldTypes.size(); i++) { rowValue.setField(i, valueMap.get(valueFieldNames.get(i))); } - rowTuple2.f1 = valueConditionAddQuotation(buildValuesCondition(fieldTypes, rowValue)); + rowTuple2.f1 = buildValuesCondition(fieldTypes, rowValue); putRowIntoMap(rowDataMap, rowTuple2); } @@ -446,11 +403,7 @@ public void close() throws IOException { } // 将还未执行的SQL flush if (batchCount > 0) { - try { - flush(); - } catch (Exception e) { - throw new RuntimeException("Writing records to impala failed.", e); - } + flush(); } // cancel scheduled task if (this.scheduledFuture != null) { @@ -484,7 +437,6 @@ public void close() throws IOException { * execute batch sql from row data map * sql like 'insert into tableName(f1, f2, f3) ${partitionCondition} values(v1, v2, v3), (v4, v5, v6).... * - * @param statement execute statement * @param tempSql template sql * @param storeType the store type of data * @param enablePartition enable partition or not @@ -492,8 +444,7 @@ public void close() throws IOException { * @param partitionFields partition fields * @param rowDataMap row data map */ - private void executeBatchSql(Statement statement, - String tempSql, + private void executeBatchSql(String tempSql, String schema, String tableName, String storeType, @@ -501,46 +452,83 @@ private void executeBatchSql(Statement statement, List fieldNames, String partitionFields, Map> rowDataMap) { - StringBuilder valuesCondition = new StringBuilder(); StringBuilder partitionCondition = new StringBuilder(); String tableFieldsCondition = buildTableFieldsCondition(fieldNames, partitionFields); - ArrayList rowData; + ArrayList rowData = new ArrayList<>(); String tableNameInfo = Objects.isNull(schema) ? - tableName : quoteIdentifier(schema) + "." + tableName; + tableName : quoteIdentifier(schema) + "." + tableName; tempSql = tempSql.replace("tableName", tableNameInfo); + boolean isPartitioned = storeType.equalsIgnoreCase(KUDU_TYPE) || !enablePartition; - // kudu ${partitionCondition} is null - if (storeType.equalsIgnoreCase(KUDU_TYPE) || !enablePartition) { - try { - rowData = rowDataMap.get(NO_PARTITION); - rowData.forEach(row -> valuesCondition.append(row).append(", ")); - String executeSql = tempSql.replace(VALUES_CONDITION, valuesCondition.toString()) + try { + // kudu ${partitionCondition} is null + if (isPartitioned) { + tempSql = tempSql + .replace(PARTITION_CONDITION, partitionCondition.toString()) + .replace(PARTITION_CONSTANT, "") + .replace(TABLE_FIELDS_CONDITION, tableFieldsCondition); + rowData.addAll(rowDataMap.get(NO_PARTITION)); + String executeSql = tempSql.replace(VALUES_CONDITION, String.join(", ", rowData)); + statement.execute(executeSql); + rowData.clear(); + } else { + // partition sql + Set keySet = rowDataMap.keySet(); + for (String key : keySet) { + rowData.addAll(rowDataMap.get(key)); + partitionCondition.append(key); + tempSql = tempSql .replace(PARTITION_CONDITION, partitionCondition.toString()) - .replace(PARTITION_CONSTANT, "") .replace(TABLE_FIELDS_CONDITION, tableFieldsCondition); - String substring = executeSql.substring(0, executeSql.length() - 2); - statement.execute(substring); - } catch (Exception e) { - throw new RuntimeException("execute impala SQL error!", e); + String executeSql = tempSql + .replace(VALUES_CONDITION, String.join(", ", rowData)); + statement.execute(executeSql); + partitionCondition.delete(0, partitionCondition.length()); + } } - return; + } catch (Exception e) { + if (e instanceof SQLException) { + dealBatchSqlError(rowData, connection, statement, tempSql); + } else { + throw new RuntimeException("Insert into impala error!", e); + } + } finally { + rowData.clear(); } + } - // partition sql - Set keySet = rowDataMap.keySet(); - String finalTempSql = tempSql; - for (String key : keySet) { + /** + * 当批量写入失败时,把批量的sql拆解为单条sql提交,对于单条写入的sql记做脏数据 + * + * @param rowData 批量的values + * @param connection 当前数据库connect + * @param statement 当前statement + * @param templateSql 模版sql,例如insert into tableName(f1, f2, f3) [partition] values $valueCondition + */ + private void dealBatchSqlError(List rowData, + Connection connection, + Statement statement, + String templateSql) { + String errorMsg = "Insert into impala error. \nCause: [%s]\nRow: [%s]"; + JDBCUtils.rollBack(connection); + JDBCUtils.commit(connection); + for (String rowDatum : rowData) { + String executeSql = templateSql.replace(VALUES_CONDITION, rowDatum); try { - String executeSql = String.copyValueOf(finalTempSql.toCharArray()); - ArrayList valuesConditionList = rowDataMap.get(key); - partitionCondition.append(key); - executeSql = executeSql.replace(PARTITION_CONDITION, partitionCondition.toString()) - .replace(TABLE_FIELDS_CONDITION, tableFieldsCondition) - .replace(VALUES_CONDITION, String.join(", ", valuesConditionList)); statement.execute(executeSql); - partitionCondition.delete(0, partitionCondition.length()); - } catch (SQLException sqlException) { - throw new RuntimeException("execute impala SQL error! ", sqlException); + JDBCUtils.commit(connection); + } catch (SQLException e) { + JDBCUtils.rollBack(connection); + JDBCUtils.commit(connection); + if (metricOutputFormat.outDirtyRecords.getCount() % DIRTY_DATA_PRINT_FREQUENCY == 0 || LOG.isDebugEnabled()) { + LOG.error( + String.format( + errorMsg, + ExceptionTrace.traceOriginalCause(e), + rowDatum) + ); + } + metricOutputFormat.outDirtyRecords.inc(); } } } @@ -573,9 +561,9 @@ private String buildPartitionCondition(Map rowData, String parti */ private String buildTableFieldsCondition(List fieldNames, String partitionFields) { return "(" + fieldNames.stream() - .filter(f -> !partitionFields.contains(f)) - .map(this::quoteIdentifier) - .collect(Collectors.joining(", ")) + ")"; + .filter(f -> !partitionFields.contains(f)) + .map(this::quoteIdentifier) + .collect(Collectors.joining(", ")) + ")"; } /** @@ -583,22 +571,27 @@ private String buildTableFieldsCondition(List fieldNames, String partiti * replace ${valuesCondition} * * @param fieldTypes field types - * @return condition like '(?, ?, cast(? as string))' and '?' will be replaced with row data + * @return condition like '(?, ?, cast('?' as string))' and '?' will be replaced with row data */ private String buildValuesCondition(List fieldTypes, Row row) { String valuesCondition = fieldTypes.stream().map( - f -> { - for (String item : NEED_QUOTE_TYPE) { - if (f.toLowerCase().contains(item)) { - return String.format("cast(? as %s)", f.toLowerCase()); - } + f -> { + for (String item : NEED_QUOTE_TYPE) { + if (f.toLowerCase().contains(item)) { + return String.format("cast('?' as %s)", f.toLowerCase()); } - return "?"; - }).collect(Collectors.joining(", ")); + } + return "?"; + }).collect(Collectors.joining(", ")); for (int i = 0; i < row.getArity(); i++) { - valuesCondition = valuesCondition.replaceFirst("\\?", Objects.isNull(row.getField(i)) ? "null" : row.getField(i).toString()); + Object rowField = row.getField(i); + if (DtStringUtil.isEmptyOrNull(rowField)) { + valuesCondition = valuesCondition.replaceFirst("'\\?'", "null"); + } else { + valuesCondition = valuesCondition.replaceFirst("\\?", rowField.toString()); + } } - return valuesCondition; + return "(" + valuesCondition + ")"; } /** @@ -609,16 +602,16 @@ private String buildValuesCondition(List fieldTypes, Row row) { private String buildUpdateSql(String schema, String tableName, List fieldNames, List primaryKeys) { //跳过primary key字段 String setClause = fieldNames.stream() - .filter(f -> !CollectionUtils.isNotEmpty(primaryKeys) || !primaryKeys.contains(f)) - .map(f -> quoteIdentifier(f) + "=?") - .collect(Collectors.joining(", ")); + .filter(f -> !CollectionUtils.isNotEmpty(primaryKeys) || !primaryKeys.contains(f)) + .map(f -> quoteIdentifier(f) + "=?") + .collect(Collectors.joining(", ")); String conditionClause = primaryKeys.stream() - .map(f -> quoteIdentifier(f) + "=?") - .collect(Collectors.joining(" AND ")); + .map(f -> quoteIdentifier(f) + "=?") + .collect(Collectors.joining(" AND ")); return "UPDATE " + (Objects.isNull(schema) ? "" : quoteIdentifier(schema) + ".") - + quoteIdentifier(tableName) + " SET " + setClause + " WHERE " + conditionClause; + + quoteIdentifier(tableName) + " SET " + setClause + " WHERE " + conditionClause; } private String quoteIdentifier(String identifier) { @@ -664,7 +657,7 @@ public Builder setTableName(String tableName) { public Builder setPartitionFields(String partitionFields) { format.partitionFields = Objects.isNull(partitionFields) ? - "" : partitionFields; + "" : partitionFields; return this; } @@ -739,11 +732,11 @@ public ImpalaOutputFormat build() { if (format.authMech == EAuthMech.Kerberos.getType()) { checkNotNull(format.krb5confPath, - "When kerberos authentication is enabled, krb5confPath is required!"); + "When kerberos authentication is enabled, krb5confPath is required!"); checkNotNull(format.principal, - "When kerberos authentication is enabled, principal is required!"); + "When kerberos authentication is enabled, principal is required!"); checkNotNull(format.keytabPath, - "When kerberos authentication is enabled, keytabPath is required!"); + "When kerberos authentication is enabled, keytabPath is required!"); } if (format.authMech == EAuthMech.UserName.getType()) { From 758c4db4b62fe2c5630058496989a99fdbfd9a51 Mon Sep 17 00:00:00 2001 From: mowen Date: Wed, 10 Mar 2021 14:52:25 +0800 Subject: [PATCH 100/103] [hotfix-34789][elasticsearch5&6][sink]support timestamp datatype. --- .../java/com/dtstack/flink/sql/enums/ColumnType.java | 4 ++++ .../java/com/dtstack/flink/sql/util/DtStringUtil.java | 3 +++ .../sql/sink/elasticsearch/CustomerSinkFunc.java | 11 +++++++++++ .../sql/sink/elasticsearch/CustomerSinkFunc.java | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/core/src/main/java/com/dtstack/flink/sql/enums/ColumnType.java b/core/src/main/java/com/dtstack/flink/sql/enums/ColumnType.java index 7f3f0019c..e93c92f51 100644 --- a/core/src/main/java/com/dtstack/flink/sql/enums/ColumnType.java +++ b/core/src/main/java/com/dtstack/flink/sql/enums/ColumnType.java @@ -102,6 +102,10 @@ public enum ColumnType { * timestamp */ TIMESTAMP, + /** + * time eg: 23:59:59 + */ + TIME, /** * decimal */ diff --git a/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java b/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java index dbf6d9e7b..bfa0c7ca5 100644 --- a/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java +++ b/core/src/main/java/com/dtstack/flink/sql/util/DtStringUtil.java @@ -243,6 +243,9 @@ public static String col2string(Object column, String type) { case DATE: result = DateUtil.dateToString((java.util.Date)column); break; + case TIME: + result = DateUtil.getTimeFromStr(String.valueOf(column)); + break; case TIMESTAMP: result = DateUtil.timestampToString((java.util.Date)column); break; diff --git a/elasticsearch5/elasticsearch5-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/CustomerSinkFunc.java b/elasticsearch5/elasticsearch5-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/CustomerSinkFunc.java index 23213e721..9c68ffea7 100644 --- a/elasticsearch5/elasticsearch5-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/CustomerSinkFunc.java +++ b/elasticsearch5/elasticsearch5-sink/src/main/java/com/dtstack/flink/sql/sink/elasticsearch/CustomerSinkFunc.java @@ -20,6 +20,7 @@ package com.dtstack.flink.sql.sink.elasticsearch; +import com.dtstack.flink.sql.util.DateUtil; import org.apache.commons.lang3.StringUtils; import org.apache.flink.api.common.functions.RuntimeContext; import org.apache.flink.api.java.tuple.Tuple2; @@ -32,6 +33,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.sql.Date; +import java.sql.Timestamp; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -107,6 +110,14 @@ private IndexRequest createIndexRequest(Row element) { Map dataMap = EsUtil.rowToJsonMap(element,fieldNames,fieldTypes); int length = Math.min(element.getArity(), fieldNames.size()); for(int i=0; i Date: Mon, 22 Mar 2021 10:00:53 +0800 Subject: [PATCH 101/103] =?UTF-8?q?[hotfix-35753][impala]=20fix=20String.r?= =?UTF-8?q?eplaceFirst=E4=B8=AD=E7=9A=84=E5=9D=91=EF=BC=8CreplaceFirst("\\?= =?UTF-8?q?=3F",=20value)=EF=BC=8C=E5=A6=82=E6=9E=9Cvalue=E4=B8=AD$?= =?UTF-8?q?=E5=90=8E=E4=B8=8D=E6=98=AF=E6=95=B0=E5=AD=97=EF=BC=8C=E9=82=A3?= =?UTF-8?q?=E4=B9=88=E4=BC=9A=E5=87=BA=E7=8E=B0"illegal=20group=20referenc?= =?UTF-8?q?e=EF=BC=9Agroup=20index=20is=20missing"=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java index 82122e00f..8723254bf 100644 --- a/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java +++ b/impala/impala-sink/src/main/java/com/dtstack/flink/sql/sink/impala/ImpalaOutputFormat.java @@ -588,7 +588,7 @@ private String buildValuesCondition(List fieldTypes, Row row) { if (DtStringUtil.isEmptyOrNull(rowField)) { valuesCondition = valuesCondition.replaceFirst("'\\?'", "null"); } else { - valuesCondition = valuesCondition.replaceFirst("\\?", rowField.toString()); + valuesCondition = valuesCondition.replaceFirst("\\?", Matcher.quoteReplacement(rowField.toString())); } } return "(" + valuesCondition + ")"; From 1297b4fa75c206c32ded2c8fa99df8d5ca03b7bb Mon Sep 17 00:00:00 2001 From: xiuzhu Date: Tue, 13 Apr 2021 17:13:08 +0800 Subject: [PATCH 102/103] =?UTF-8?q?[hotfix-36198][redis]=20=E8=A7=A3?= =?UTF-8?q?=E5=86=B3redis=E5=BC=82=E6=AD=A5=E7=BB=B4=E8=A1=A8cluster?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E8=BF=9E=E6=8E=A5=E5=BC=82=E5=B8=B8=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hbase/hbase-side/hbase-all-side/pom.xml | 4 +++ hbase/pom.xml | 4 +++ .../sql/side/redis/RedisAsyncReqRow.java | 27 ++++++++++++++++--- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/hbase/hbase-side/hbase-all-side/pom.xml b/hbase/hbase-side/hbase-all-side/pom.xml index cdfc24b74..dcc5a1987 100644 --- a/hbase/hbase-side/hbase-all-side/pom.xml +++ b/hbase/hbase-side/hbase-all-side/pom.xml @@ -33,6 +33,10 @@ io.netty netty + + netty-all + io.netty + diff --git a/hbase/pom.xml b/hbase/pom.xml index 835457e29..9c82560b3 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -47,6 +47,10 @@ io.netty netty + + netty-all + io.netty + diff --git a/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java b/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java index 395aa50a5..5fc8f62b9 100644 --- a/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java +++ b/redis5/redis5-side/redis-async-side/src/main/java/com/dtstack/flink/sql/side/redis/RedisAsyncReqRow.java @@ -36,6 +36,7 @@ import io.lettuce.core.api.async.RedisKeyAsyncCommands; import io.lettuce.core.cluster.RedisClusterClient; import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; +import io.lettuce.core.internal.HostAndPort; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang.StringUtils; import org.apache.flink.api.java.typeutils.RowTypeInfo; @@ -44,11 +45,11 @@ import org.apache.flink.table.runtime.types.CRow; import org.apache.flink.types.Row; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.function.Consumer; import java.util.regex.Matcher; /** @@ -133,9 +134,8 @@ private void buildRedisClient(RedisSideTableInfo tableInfo){ async = connection.async(); break; case CLUSTER: - RedisURI clusterURI = RedisURI.create("redis://" + url); - clusterURI.setPassword(password); - clusterClient = RedisClusterClient.create(clusterURI); + List clusterURIs = buildClusterURIs(url); + clusterClient = RedisClusterClient.create(clusterURIs); clusterConnection = clusterClient.connect(); async = clusterConnection.async(); default: @@ -143,6 +143,25 @@ private void buildRedisClient(RedisSideTableInfo tableInfo){ } } + private List buildClusterURIs(String url) { + String password = redisSideTableInfo.getPassword(); + String database = redisSideTableInfo.getDatabase(); + String[] addresses = StringUtils.split(url, ","); + List redisURIs = new ArrayList<>(addresses.length); + for (String addr : addresses){ + HostAndPort hostAndPort = HostAndPort.parse(addr); + RedisURI redisURI = RedisURI.create(hostAndPort.hostText, hostAndPort.port); + if (StringUtils.isNotEmpty(password)) { + redisURI.setPassword(password); + } + if (StringUtils.isNotEmpty(database)) { + redisURI.setDatabase(Integer.valueOf(database)); + } + redisURIs.add(redisURI); + } + return redisURIs; + } + private RedisURI.Builder buildSentinelUri( String host, String port, From 34e200b58fb8a89bedb2f3228f978fc123bdbf47 Mon Sep 17 00:00:00 2001 From: xiuzhu Date: Thu, 15 Apr 2021 18:51:19 +0800 Subject: [PATCH 103/103] [hotfix-36198] redis async side shade netty --- hbase/hbase-side/hbase-all-side/pom.xml | 4 ---- hbase/pom.xml | 4 ---- redis5/redis5-side/redis-async-side/pom.xml | 8 +++++++- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/hbase/hbase-side/hbase-all-side/pom.xml b/hbase/hbase-side/hbase-all-side/pom.xml index dcc5a1987..cdfc24b74 100644 --- a/hbase/hbase-side/hbase-all-side/pom.xml +++ b/hbase/hbase-side/hbase-all-side/pom.xml @@ -33,10 +33,6 @@ io.netty netty - - netty-all - io.netty - diff --git a/hbase/pom.xml b/hbase/pom.xml index 9c82560b3..835457e29 100644 --- a/hbase/pom.xml +++ b/hbase/pom.xml @@ -47,10 +47,6 @@ io.netty netty - - netty-all - io.netty - diff --git a/redis5/redis5-side/redis-async-side/pom.xml b/redis5/redis5-side/redis-async-side/pom.xml index 39a6280cc..666bace2b 100644 --- a/redis5/redis5-side/redis-async-side/pom.xml +++ b/redis5/redis5-side/redis-async-side/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-shade-plugin - 1.4 + 3.0.0 package @@ -43,6 +43,12 @@ shade + + + io.netty + com.dtstack.flink.sql.side.redis.io.netty + +