├── .gitignore
├── LICENSE
├── README.md
├── flow.png
├── odar-mysql-connector
├── pom.xml
└── src
│ └── main
│ └── java
│ └── io
│ └── openmessaging
│ └── mysql
│ ├── Config.java
│ ├── MysqlConstants.java
│ ├── Replicator.java
│ ├── binlog
│ ├── DataRow.java
│ ├── EventListener.java
│ ├── EventProcessor.java
│ └── Transaction.java
│ ├── connector
│ ├── MysqlConnector.java
│ └── MysqlTask.java
│ ├── position
│ ├── BinlogPosition.java
│ ├── BinlogPositionLogThread.java
│ └── BinlogPositionManager.java
│ └── schema
│ ├── Database.java
│ ├── Schema.java
│ ├── Table.java
│ └── column
│ ├── BigIntColumnParser.java
│ ├── ColumnParser.java
│ ├── DateTimeColumnParser.java
│ ├── DefaultColumnParser.java
│ ├── EnumColumnParser.java
│ ├── IntColumnParser.java
│ ├── SetColumnParser.java
│ ├── StringColumnParser.java
│ ├── TimeColumnParser.java
│ └── YearColumnParser.java
└── pom.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | *.class
3 | *.jar
4 | *dependency-reduced-pom.xml
5 | .classpath
6 | .project
7 | .settings/
8 | target/
9 | devenv
10 | *.log*
11 | *.iml
12 | .idea/
13 | *.versionsBackup
14 | .DS_Store
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2019 Alibaba
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OpenMessaging Connect
2 |
3 | ## Introduction
4 | OpenMessaging Connect is a standard to connect between data sources and data destinations. Users could easily create connector instances with configurations via REST API.
5 |
6 | There are two types of connectors: source connector and sink connector. A source connector is used for pulling data from a data source (e.g. RDBMS).
7 | The data is sent to corresponding message queue and expected to be consumed by one or many sink connectors.
8 | A sink connector receives message from the queue and loads into a data destination (e.g. data warehouse).
9 | Developers should implement source or sink connector interface to run their specific job.
10 |
11 | Usually, connectors rely on a concrete message queue for data transportation. The message queue decouples source connectors from sink connectors.
12 | In the meantime, it provides capabilities such as failover, rate control and one to many data transportation etc.
13 | Some message queues (e.g. Kafka) provide bundled connect frameworks and a various of connectors developed officially or by the community.
14 | However, these frameworks are lack of interoperability, which means a connector developed for Kafka cannot run with
15 | RabbitMQ without modification and vice versa.
16 |
17 | 
18 |
19 | A connector follows OpenMessaging Connect could run with any message queues which support OpenMessaging API.
20 | OpenMessaging Connect provides a standalone runtime which uses OpenMessaging API for sending and consuming message,
21 | as well as the key/value operations for offset management.
22 |
23 | ## Connector
24 |
25 | This runtime is based on interface [openmessaging-connector](https://github.com/openmessaging/openmessaging-connector).
26 |
27 | ## Runtime quick start
28 |
29 | ### Prerequisite
30 |
31 | * 64bit JDK 1.8+;
32 | * Maven 3.2.x;
33 | * A running MQ cluster;
34 |
35 | ### Build
36 |
37 | ```
38 | mvn clean install -Dmaven.test.skip=true
39 | ```
40 |
41 | ### Run Command Line
42 |
43 | ```
44 | ## Enter runtime directory
45 | cd runtime/
46 |
47 | ## run run_worker.sh
48 | sh ./run_worker.sh
49 | ```
50 |
51 | ### Log Path
52 |
53 | Default path is:
54 | ```
55 | ${user.home}/logs/omsconnect/
56 | ```
57 |
58 | If you see "The worker XXX boot success." without any exception, it means worker started successfully.
59 |
60 | ### Make Sure Worker Started Successfully
61 |
62 | Use http request to get all alive workers in cluster:
63 | ```
64 | GET /getClusterInfo
65 | ```
66 | You will see all alive workers' id with latest heartbeat timestamp.
67 |
68 | ### Run The Example Mysql Source Connector
69 |
70 | Use the following http request, to start a mysql source connector as an example.
71 | ```
72 | GET http://(your worker ip):(port)/connectors/(Your connector name)?config={"connector-class":"io.openmessaging.mysql.connector.MysqlConnector","oms-driver-url":"oms:rocketmq://localhost:9876/default:default","mysqlAddr":"localhost","mysqlPort":"3306","mysqlUsername":"username","mysqlPassword":"password","source-record-converter":"io.openmessaging.connect.runtime.converter.JsonConverter"}
73 | ```
74 | Note to replace the arguments in "()" with your own mysql setting.
75 |
76 | #### Config introduction
77 |
78 | |key |nullable|default |description|
79 | |------------------|--------|-----------|-----------|
80 | |connector-class |false | |Full class name of the impl of connector|
81 | |oms-driver-url |false | |An OMS driver url, the data will send to the specified MQ|
82 | |mysqlAddr |false | |MySQL address|
83 | |mysqlPort |false | |MySQL port|
84 | |mysqlUsername |false | |Username of MySQL account|
85 | |mysqlPassword |false | |Password of MySQL account|
86 | |source-record-converter |false | |Full class name of the impl of the converter used to convert SourceDataEntry to byte[]|
87 | |whiteDataBase |false | |DataBase Name which you want to source data
88 | |whiteTable |true | |Table Name which you want to source data (choosable)
89 |
90 | ### Verify Mysql Source Connector Started Successfully
91 |
92 | Make a mysql row update, your will found the MQ you config receive a message, and the message's topic name is the table name.
93 |
94 | ## Note
95 |
96 | Module odar-mysql-connector is just a simple example of source connector.
97 | This module is base on [RocketMQ Mysql](https://github.com/apache/rocketmq-externals/tree/master/rocketmq-mysql) with a little code modified in order to implement connector interface.
98 | User can reference this module to implement other source or sink connector.
99 |
100 | ## RoadMap
101 |
102 | 1. Implement WorkerSinkTask wrapper in runtime;
103 | 2. Import openMessaging impl and connector impl dynamically in runtime, rather than import them in pom file;
104 | 3. An overall optimization of runtime module, provide a better interaction.
105 | 4. Implementation of various source and sink connector.
--------------------------------------------------------------------------------
/flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openmessaging/openmessaging-connect-odar/dec822705b1130f17b869bd39e641372f88fdbee/flow.png
--------------------------------------------------------------------------------
/odar-mysql-connector/pom.xml:
--------------------------------------------------------------------------------
1 |
17 |
20 |
21 | io.openmessaging
22 | openmessaging-odar
23 | 0.0.1-SNAPSHOT
24 |
25 | 4.0.0
26 |
27 | odar-mysql-connector
28 |
29 |
30 | io.openmessaging
31 | openmessaging-api
32 | 0.3.1-alpha
33 |
34 |
35 | com.github.shyiko
36 | mysql-binlog-connector-java
37 | 0.12.1
38 |
39 |
40 | mysql
41 | mysql-connector-java
42 | 8.0.11
43 |
44 |
45 | org.slf4j
46 | slf4j-api
47 | 1.7.5
48 |
49 |
50 | ch.qos.logback
51 | logback-classic
52 | 1.0.13
53 |
54 |
55 | com.alibaba
56 | druid
57 | 1.0.31
58 |
59 |
60 | commons-codec
61 | commons-codec
62 | 1.9
63 |
64 |
65 | junit
66 | junit
67 | 4.11
68 | test
69 |
70 |
71 | com.alibaba
72 | fastjson
73 |
74 |
75 | io.openmessaging
76 | openmessaging-connector
77 | 0.1.0-beta
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/odar-mysql-connector/src/main/java/io/openmessaging/mysql/Config.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Licensed to the Apache Software Foundation (ASF) under one or more
3 | * contributor license agreements. See the NOTICE file distributed with
4 | * this work for additional information regarding copyright ownership.
5 | * The ASF licenses this file to You under the Apache License, Version 2.0
6 | * (the "License"); you may not use this file except in compliance with
7 | * the License. You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | package io.openmessaging.mysql;
19 |
20 | import io.openmessaging.KeyValue;
21 | import java.io.IOException;
22 | import java.io.InputStream;
23 | import java.lang.reflect.Method;
24 | import java.util.HashSet;
25 | import java.util.Properties;
26 | import java.util.Set;
27 |
28 | public class Config {
29 |
30 | public String mysqlAddr;
31 | public Integer mysqlPort;
32 | public String mysqlUsername;
33 | public String mysqlPassword;
34 | public String whiteDataBase;
35 | public String whiteTable;
36 |
37 | public String queueName;
38 |
39 | public String startType = "DEFAULT";
40 | public String binlogFilename;
41 | public Long nextPosition;
42 | public Integer maxTransactionRows = 100;
43 |
44 | public static final Set REQUEST_CONFIG = new HashSet(){
45 | {
46 | add("mysqlAddr");
47 | add("mysqlPort");
48 | add("mysqlUsername");
49 | add("mysqlPassword");
50 | add("whiteDataBase");
51 | }
52 | };
53 |
54 | public void load(KeyValue props) {
55 |
56 | properties2Object(props, this);
57 | }
58 |
59 | private void properties2Object(final KeyValue p, final Object object) {
60 |
61 | Method[] methods = object.getClass().getMethods();
62 | for (Method method : methods) {
63 | String mn = method.getName();
64 | if (mn.startsWith("set")) {
65 | try {
66 | String tmp = mn.substring(4);
67 | String first = mn.substring(3, 4);
68 |
69 | String key = first.toLowerCase() + tmp;
70 | String property = p.getString(key);
71 | if (property != null) {
72 | Class>[] pt = method.getParameterTypes();
73 | if (pt != null && pt.length > 0) {
74 | String cn = pt[0].getSimpleName();
75 | Object arg;
76 | if (cn.equals("int") || cn.equals("Integer")) {
77 | arg = Integer.parseInt(property);
78 | } else if (cn.equals("long") || cn.equals("Long")) {
79 | arg = Long.parseLong(property);
80 | } else if (cn.equals("double") || cn.equals("Double")) {
81 | arg = Double.parseDouble(property);
82 | } else if (cn.equals("boolean") || cn.equals("Boolean")) {
83 | arg = Boolean.parseBoolean(property);
84 | } else if (cn.equals("float") || cn.equals("Float")) {
85 | arg = Float.parseFloat(property);
86 | } else if (cn.equals("String")) {
87 | arg = property;
88 | } else {
89 | continue;
90 | }
91 | method.invoke(object, arg);
92 | }
93 | }
94 | } catch (Throwable ignored) {
95 | }
96 | }
97 | }
98 | }
99 |
100 | public void setMysqlAddr(String mysqlAddr) {
101 | this.mysqlAddr = mysqlAddr;
102 | }
103 |
104 | public void setMysqlPort(Integer mysqlPort) {
105 | this.mysqlPort = mysqlPort;
106 | }
107 |
108 | public void setMysqlUsername(String mysqlUsername) {
109 | this.mysqlUsername = mysqlUsername;
110 | }
111 |
112 | public void setMysqlPassword(String mysqlPassword) {
113 | this.mysqlPassword = mysqlPassword;
114 | }
115 |
116 | public void setBinlogFilename(String binlogFilename) {
117 | this.binlogFilename = binlogFilename;
118 | }
119 |
120 | public void setNextPosition(Long nextPosition) {
121 | this.nextPosition = nextPosition;
122 | }
123 |
124 | public void setMaxTransactionRows(Integer maxTransactionRows) {
125 | this.maxTransactionRows = maxTransactionRows;
126 | }
127 |
128 | public void setStartType(String startType) {
129 | this.startType = startType;
130 | }
131 |
132 | public void setQueueName(String queueName) {
133 | this.queueName = queueName;
134 | }
135 |
136 | public String getMysqlAddr() {
137 | return mysqlAddr;
138 | }
139 |
140 | public Integer getMysqlPort() {
141 | return mysqlPort;
142 | }
143 |
144 | public String getMysqlUsername() {
145 | return mysqlUsername;
146 | }
147 |
148 | public String getMysqlPassword() {
149 | return mysqlPassword;
150 | }
151 |
152 | public String getWhiteDataBase() {
153 | return whiteDataBase;
154 | }
155 |
156 | public void setWhiteDataBase(String whiteDataBase) {
157 | this.whiteDataBase = whiteDataBase;
158 | }
159 |
160 | public String getWhiteTable() {
161 | return whiteTable;
162 | }
163 |
164 | public void setWhiteTable(String whiteTable) {
165 | this.whiteTable = whiteTable;
166 | }
167 |
168 | public String getQueueName() {
169 | return queueName;
170 | }
171 |
172 | public String getStartType() {
173 | return startType;
174 | }
175 |
176 | public String getBinlogFilename() {
177 | return binlogFilename;
178 | }
179 |
180 | public Long getNextPosition() {
181 | return nextPosition;
182 | }
183 |
184 | public Integer getMaxTransactionRows() {
185 | return maxTransactionRows;
186 | }
187 |
188 | public static Set getRequestConfig() {
189 | return REQUEST_CONFIG;
190 | }
191 | }
--------------------------------------------------------------------------------
/odar-mysql-connector/src/main/java/io/openmessaging/mysql/MysqlConstants.java:
--------------------------------------------------------------------------------
1 | package io.openmessaging.mysql;
2 |
3 | public class MysqlConstants {
4 |
5 | public static final String BINLOG_FILENAME = "binlogFilename";
6 |
7 | public static final String NEXT_POSITION = "nextPosition";
8 |
9 | public static String getPartition(String mysqlAddr, int mysqlPort){
10 | return mysqlAddr+mysqlPort;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/odar-mysql-connector/src/main/java/io/openmessaging/mysql/Replicator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Licensed to the Apache Software Foundation (ASF) under one or more
3 | * contributor license agreements. See the NOTICE file distributed with
4 | * this work for additional information regarding copyright ownership.
5 | * The ASF licenses this file to You under the Apache License, Version 2.0
6 | * (the "License"); you may not use this file except in compliance with
7 | * the License. You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | package io.openmessaging.mysql;
19 |
20 | import io.openmessaging.mysql.position.BinlogPosition;
21 | import java.util.concurrent.BlockingQueue;
22 | import java.util.concurrent.LinkedBlockingQueue;
23 | import io.openmessaging.mysql.binlog.EventProcessor;
24 | import io.openmessaging.mysql.binlog.Transaction;
25 | import org.slf4j.Logger;
26 | import org.slf4j.LoggerFactory;
27 |
28 | public class Replicator {
29 |
30 | private static final Logger LOGGER = LoggerFactory.getLogger(Replicator.class);
31 |
32 | private static final Logger POSITION_LOGGER = LoggerFactory.getLogger("PositionLogger");
33 |
34 | private Config config;
35 |
36 | private EventProcessor eventProcessor;
37 |
38 | private Object lock = new Object();
39 | private BinlogPosition nextBinlogPosition;
40 | private long nextQueueOffset;
41 | private long xid;
42 | private BlockingQueue queue = new LinkedBlockingQueue<>();
43 |
44 | public Replicator(Config config){
45 | this.config = config;
46 | }
47 |
48 | public void start() {
49 |
50 | try {
51 |
52 | eventProcessor = new EventProcessor(this);
53 | eventProcessor.start();
54 |
55 | } catch (Exception e) {
56 | LOGGER.error("Start error.", e);
57 | }
58 | }
59 |
60 | public void stop(){
61 | eventProcessor.stop();
62 | }
63 |
64 | public void commit(Transaction transaction, boolean isComplete) {
65 |
66 | queue.add(transaction);
67 | for (int i = 0; i < 3; i++) {
68 | try {
69 | if (isComplete) {
70 | long offset = 1;
71 | synchronized (lock) {
72 | xid = transaction.getXid();
73 | nextBinlogPosition = transaction.getNextBinlogPosition();
74 | nextQueueOffset = offset;
75 | }
76 |
77 | } else {
78 | }
79 | break;
80 |
81 | } catch (Exception e) {
82 | LOGGER.error("Push error,retry:" + (i + 1) + ",", e);
83 | }
84 | }
85 | }
86 |
87 | public void logPosition() {
88 |
89 | String binlogFilename = null;
90 | long xid = 0L;
91 | long nextPosition = 0L;
92 | long nextOffset = 0L;
93 |
94 | synchronized (lock) {
95 | if (nextBinlogPosition != null) {
96 | xid = this.xid;
97 | binlogFilename = nextBinlogPosition.getBinlogFilename();
98 | nextPosition = nextBinlogPosition.getPosition();
99 | nextOffset = nextQueueOffset;
100 | }
101 | }
102 |
103 | if (binlogFilename != null) {
104 | POSITION_LOGGER.info("XID: {}, BINLOG_FILE: {}, NEXT_POSITION: {}, NEXT_OFFSET: {}",
105 | xid, binlogFilename, nextPosition, nextOffset);
106 | }
107 |
108 | }
109 |
110 | public Config getConfig() {
111 | return config;
112 | }
113 |
114 | public BinlogPosition getNextBinlogPosition() {
115 | return nextBinlogPosition;
116 | }
117 |
118 | public BlockingQueue getQueue() {
119 | return queue;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/odar-mysql-connector/src/main/java/io/openmessaging/mysql/binlog/DataRow.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Licensed to the Apache Software Foundation (ASF) under one or more
3 | * contributor license agreements. See the NOTICE file distributed with
4 | * this work for additional information regarding copyright ownership.
5 | * The ASF licenses this file to You under the Apache License, Version 2.0
6 | * (the "License"); you may not use this file except in compliance with
7 | * the License. You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | package io.openmessaging.mysql.binlog;
19 |
20 | import io.openmessaging.connector.api.data.EntryType;
21 | import io.openmessaging.mysql.schema.Table;
22 | import io.openmessaging.mysql.schema.column.ColumnParser;
23 | import java.io.Serializable;
24 | import java.util.HashMap;
25 | import java.util.List;
26 | import java.util.Map;
27 | import org.slf4j.Logger;
28 | import org.slf4j.LoggerFactory;
29 |
30 | public class DataRow {
31 |
32 | private Logger logger = LoggerFactory.getLogger(DataRow.class);
33 |
34 | private EntryType type;
35 | private Table table;
36 | private Serializable[] rowBeforeUpdate;
37 | private Serializable[] row;
38 |
39 | public DataRow(EntryType type, Table table, Serializable[] row, Serializable[] rowBeforeUpdate) {
40 | this.type = type;
41 | this.table = table;
42 | this.row = row;
43 | this.rowBeforeUpdate = rowBeforeUpdate;
44 | }
45 |
46 | public Map toMap() {
47 |
48 | try {
49 | if (table.getColList().size() == row.length) {
50 | Map beforeDataMap = new HashMap<>();
51 | Map dataMap = new HashMap<>();
52 | List keyList = table.getColList();
53 | List parserList = table.getParserList();
54 |
55 | for (int i = 0; i < keyList.size(); i++) {
56 | ColumnParser parser = parserList.get(i);
57 | if(null != row){
58 | Object value = row[i];
59 | dataMap.put(keyList.get(i), parser.getValue(value));
60 | }
61 | if(null != rowBeforeUpdate){
62 | beforeDataMap.put(keyList.get(i), parser.getValue(rowBeforeUpdate[i]));
63 | }
64 | }
65 |
66 | Map map = new HashMap<>();
67 | map.put("database", table.getDatabase());
68 | map.put("table", table.getName());
69 | map.put("type", type);
70 | if(dataMap.size() > 0){
71 | map.put("data", dataMap);
72 | }
73 | if(beforeDataMap.size() > 0){
74 | map.put("beforeData", beforeDataMap);
75 | }
76 |
77 | return map;
78 | } else {
79 | logger.error("Table schema changed,discard data: {} - {}, {} {}",
80 | table.getDatabase().toUpperCase(), table.getName().toUpperCase(), type, row.toString());
81 |
82 | return null;
83 | }
84 | } catch (Exception e) {
85 | logger.error("Row parse error,discard data: {} - {}, {} {}",
86 | table.getDatabase().toUpperCase(), table.getName().toUpperCase(), type, row.toString());
87 | }
88 |
89 | return null;
90 | }
91 |
92 | public EntryType getType() {
93 | return type;
94 | }
95 |
96 | public Table getTable() {
97 | return table;
98 | }
99 |
100 | public Serializable[] getRow() {
101 | return row;
102 | }
103 |
104 | public Serializable[] getRowBeforeUpdate() {
105 | return rowBeforeUpdate;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/odar-mysql-connector/src/main/java/io/openmessaging/mysql/binlog/EventListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Licensed to the Apache Software Foundation (ASF) under one or more
3 | * contributor license agreements. See the NOTICE file distributed with
4 | * this work for additional information regarding copyright ownership.
5 | * The ASF licenses this file to You under the Apache License, Version 2.0
6 | * (the "License"); you may not use this file except in compliance with
7 | * the License. You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | package io.openmessaging.mysql.binlog;
19 |
20 | import com.github.shyiko.mysql.binlog.BinaryLogClient;
21 | import com.github.shyiko.mysql.binlog.event.Event;
22 | import io.openmessaging.mysql.connector.MysqlTask;
23 | import java.util.concurrent.BlockingQueue;
24 | import java.util.concurrent.TimeUnit;
25 | import org.slf4j.Logger;
26 | import org.slf4j.LoggerFactory;
27 |
28 | public class EventListener implements BinaryLogClient.EventListener, BinaryLogClient.LifecycleListener {
29 |
30 | private static final Logger log = LoggerFactory.getLogger(MysqlTask.class);
31 |
32 | private BlockingQueue queue;
33 |
34 | public EventListener(BlockingQueue queue) {
35 | this.queue = queue;
36 | }
37 |
38 | @Override
39 | public void onEvent(Event event) {
40 | try {
41 | while (true) {
42 | if (queue.offer(event, 100, TimeUnit.MILLISECONDS)) {
43 | return;
44 | }
45 | }
46 | } catch (InterruptedException e) {
47 | log.error("Mysql task EventListener#onEvent error.", e);
48 | }
49 | }
50 |
51 | @Override
52 | public void onConnect(BinaryLogClient client) {
53 |
54 | }
55 |
56 | @Override
57 | public void onCommunicationFailure(BinaryLogClient client, Exception e) {
58 |
59 | }
60 |
61 | @Override
62 | public void onEventDeserializationFailure(BinaryLogClient client, Exception e) {
63 |
64 | }
65 |
66 | @Override
67 | public void onDisconnect(BinaryLogClient client) {
68 |
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/odar-mysql-connector/src/main/java/io/openmessaging/mysql/binlog/EventProcessor.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Licensed to the Apache Software Foundation (ASF) under one or more
3 | * contributor license agreements. See the NOTICE file distributed with
4 | * this work for additional information regarding copyright ownership.
5 | * The ASF licenses this file to You under the Apache License, Version 2.0
6 | * (the "License"); you may not use this file except in compliance with
7 | * the License. You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | package io.openmessaging.mysql.binlog;
19 |
20 | import com.alibaba.druid.pool.DruidDataSourceFactory;
21 | import com.alibaba.druid.util.StringUtils;
22 | import com.github.shyiko.mysql.binlog.BinaryLogClient;
23 | import com.github.shyiko.mysql.binlog.event.DeleteRowsEventData;
24 | import com.github.shyiko.mysql.binlog.event.Event;
25 | import com.github.shyiko.mysql.binlog.event.EventHeaderV4;
26 | import com.github.shyiko.mysql.binlog.event.QueryEventData;
27 | import com.github.shyiko.mysql.binlog.event.TableMapEventData;
28 | import com.github.shyiko.mysql.binlog.event.UpdateRowsEventData;
29 | import com.github.shyiko.mysql.binlog.event.WriteRowsEventData;
30 | import com.github.shyiko.mysql.binlog.event.XidEventData;
31 | import com.github.shyiko.mysql.binlog.event.deserialization.EventDeserializer;
32 | import io.openmessaging.connector.api.data.EntryType;
33 | import io.openmessaging.mysql.Config;
34 | import io.openmessaging.mysql.Replicator;
35 | import io.openmessaging.mysql.position.BinlogPosition;
36 | import io.openmessaging.mysql.position.BinlogPositionManager;
37 | import io.openmessaging.mysql.schema.Schema;
38 | import io.openmessaging.mysql.schema.Table;
39 | import java.io.Serializable;
40 | import java.util.*;
41 | import java.util.concurrent.BlockingQueue;
42 | import java.util.concurrent.LinkedBlockingQueue;
43 | import java.util.concurrent.TimeUnit;
44 | import java.util.regex.Pattern;
45 | import javax.sql.DataSource;
46 | import org.slf4j.Logger;
47 | import org.slf4j.LoggerFactory;
48 |
49 | public class EventProcessor {
50 | private static final Logger LOGGER = LoggerFactory.getLogger(EventProcessor.class);
51 |
52 | private Replicator replicator;
53 | private Config config;
54 |
55 | private DataSource dataSource;
56 |
57 | private BinlogPositionManager binlogPositionManager;
58 |
59 | private BlockingQueue queue = new LinkedBlockingQueue<>(100);
60 |
61 | private BinaryLogClient binaryLogClient;
62 |
63 | private EventListener eventListener;
64 |
65 | private Schema schema;
66 |
67 | private Map tableMap = new HashMap<>();
68 |
69 | private Transaction transaction;
70 |
71 | private boolean stopped;
72 |
73 | public EventProcessor(Replicator replicator) {
74 |
75 | this.replicator = replicator;
76 | this.config = replicator.getConfig();
77 | }
78 |
79 | public void start() throws Exception {
80 |
81 | initDataSource();
82 |
83 | binlogPositionManager = new BinlogPositionManager(config, dataSource);
84 | binlogPositionManager.initBeginPosition();
85 |
86 | schema = new Schema(dataSource);
87 | String whiteDataBases = config.getWhiteDataBase();
88 | String whiteTables = config.getWhiteTable();
89 |
90 | if (!StringUtils.isEmpty(whiteDataBases)){
91 | Arrays.asList(whiteDataBases.trim().split(",")).forEach(whiteDataBase ->{
92 | Collections.addAll(schema.dataBaseWhiteList, whiteDataBase);
93 | });
94 | }
95 |
96 | if (!StringUtils.isEmpty(whiteTables)){
97 | Arrays.asList(whiteTables.trim().split(",")).forEach(whiteTable ->{
98 | Collections.addAll(schema.tableWhiteList, whiteTable);
99 | });
100 | }
101 | schema.load();
102 |
103 | eventListener = new EventListener(queue);
104 | binaryLogClient = new BinaryLogClient(config.mysqlAddr,
105 | config.mysqlPort,
106 | config.mysqlUsername,
107 | config.mysqlPassword);
108 | binaryLogClient.setBlocking(true);
109 | binaryLogClient.setServerId(1001);
110 |
111 | EventDeserializer eventDeserializer = new EventDeserializer();
112 | eventDeserializer.setCompatibilityMode(EventDeserializer.CompatibilityMode.DATE_AND_TIME_AS_LONG,
113 | EventDeserializer.CompatibilityMode.CHAR_AND_BINARY_AS_BYTE_ARRAY);
114 | binaryLogClient.setEventDeserializer(eventDeserializer);
115 | binaryLogClient.registerEventListener(eventListener);
116 | binaryLogClient.setBinlogFilename(binlogPositionManager.getBinlogFilename());
117 | binaryLogClient.setBinlogPosition(binlogPositionManager.getPosition());
118 |
119 | binaryLogClient.connect(3000);
120 |
121 | LOGGER.info("Started.");
122 |
123 | stopped = false;
124 |
125 | doProcess();
126 | }
127 |
128 | public void stop(){
129 | stopped = true;
130 | }
131 |
132 | private void doProcess() {
133 |
134 | new Thread(() -> {
135 | while (true) {
136 |
137 | if(stopped){
138 | break;
139 | }
140 | try {
141 | Event event = queue.poll(1000, TimeUnit.MILLISECONDS);
142 | if (event == null) {
143 | checkConnection();
144 | continue;
145 | }
146 |
147 | switch (event.getHeader().getEventType()) {
148 | case TABLE_MAP:
149 | processTableMapEvent(event);
150 | break;
151 |
152 | case WRITE_ROWS:
153 | case EXT_WRITE_ROWS:
154 | processWriteEvent(event);
155 | break;
156 |
157 | case UPDATE_ROWS:
158 | case EXT_UPDATE_ROWS:
159 | processUpdateEvent(event);
160 | break;
161 |
162 | case DELETE_ROWS:
163 | case EXT_DELETE_ROWS:
164 | processDeleteEvent(event);
165 | break;
166 |
167 | case QUERY:
168 | processQueryEvent(event);
169 | break;
170 |
171 | case XID:
172 | processXidEvent(event);
173 | break;
174 |
175 | }
176 | } catch (Exception e) {
177 | LOGGER.error("Binlog process error.", e);
178 | }
179 |
180 | }
181 | }).start();
182 |
183 |
184 | }
185 |
186 | private void checkConnection() throws Exception {
187 |
188 | if (!binaryLogClient.isConnected()) {
189 | BinlogPosition binlogPosition = replicator.getNextBinlogPosition();
190 | if (binlogPosition != null) {
191 | binaryLogClient.setBinlogFilename(binlogPosition.getBinlogFilename());
192 | binaryLogClient.setBinlogPosition(binlogPosition.getPosition());
193 | }
194 |
195 | binaryLogClient.connect(3000);
196 | }
197 | }
198 |
199 | private void processTableMapEvent(Event event) {
200 | TableMapEventData data = event.getData();
201 | String dbName = data.getDatabase();
202 | String tableName = data.getTable();
203 | Long tableId = data.getTableId();
204 |
205 | Table table = schema.getTable(dbName, tableName);
206 | if (table != null){
207 | tableMap.put(tableId, table);
208 | }
209 | }
210 |
211 | private void processWriteEvent(Event event) {
212 | WriteRowsEventData data = event.getData();
213 | Long tableId = data.getTableId();
214 | List list = data.getRows();
215 |
216 | for (Serializable[] row : list) {
217 | addRow(EntryType.CREATE, tableId, row, null);
218 | }
219 | }
220 |
221 | private void processUpdateEvent(Event event) {
222 | UpdateRowsEventData data = event.getData();
223 | Long tableId = data.getTableId();
224 | List> list = data.getRows();
225 |
226 | for (Map.Entry entry : list) {
227 | addRow(EntryType.UPDATE, tableId, entry.getValue(), entry.getKey());
228 | }
229 | }
230 |
231 | private void processDeleteEvent(Event event) {
232 | DeleteRowsEventData data = event.getData();
233 | Long tableId = data.getTableId();
234 | List list = data.getRows();
235 |
236 | for (Serializable[] row : list) {
237 | addRow(EntryType.DELETE, tableId, null, row);
238 | }
239 |
240 | }
241 |
242 | private static Pattern createTablePattern =
243 | Pattern.compile("^(CREATE|ALTER)\\s+TABLE", Pattern.CASE_INSENSITIVE);
244 |
245 | private void processQueryEvent(Event event) {
246 | QueryEventData data = event.getData();
247 | String sql = data.getSql();
248 |
249 | if (createTablePattern.matcher(sql).find()) {
250 | schema.reset();
251 | }
252 | }
253 |
254 | private void processXidEvent(Event event) {
255 | EventHeaderV4 header = event.getHeader();
256 | XidEventData data = event.getData();
257 |
258 | String binlogFilename = binaryLogClient.getBinlogFilename();
259 | Long position = header.getNextPosition();
260 | Long xid = data.getXid();
261 |
262 | BinlogPosition binlogPosition = new BinlogPosition(binlogFilename, position);
263 | transaction.setNextBinlogPosition(binlogPosition);
264 | transaction.setXid(xid);
265 |
266 | replicator.commit(transaction, true);
267 |
268 | transaction = new Transaction(config);
269 | }
270 |
271 | private void addRow(EntryType type, Long tableId, Serializable[] row, Serializable[] rowBeforeUpdate) {
272 |
273 | if (transaction == null) {
274 | transaction = new Transaction(config);
275 | }
276 |
277 | Table t = tableMap.get(tableId);
278 | if (t != null) {
279 |
280 | while (true) {
281 | if (transaction.addRow(type, t, row, rowBeforeUpdate)) {
282 | break;
283 |
284 | } else {
285 | transaction.setNextBinlogPosition(replicator.getNextBinlogPosition());
286 | replicator.commit(transaction, false);
287 | transaction = new Transaction(config);
288 | }
289 | }
290 |
291 | }
292 | }
293 |
294 | private void initDataSource() throws Exception {
295 | Map map = new HashMap<>();
296 | map.put("driverClassName", "com.mysql.jdbc.Driver");
297 | map.put("url", "jdbc:mysql://" + config.mysqlAddr + ":" + config.mysqlPort + "?useSSL=true&verifyServerCertificate=false");
298 | map.put("username", config.mysqlUsername);
299 | map.put("password", config.mysqlPassword);
300 | map.put("initialSize", "2");
301 | map.put("maxActive", "2");
302 | map.put("maxWait", "60000");
303 | map.put("timeBetweenEvictionRunsMillis", "60000");
304 | map.put("minEvictableIdleTimeMillis", "300000");
305 | map.put("validationQuery", "SELECT 1 FROM DUAL");
306 | map.put("testWhileIdle", "true");
307 |
308 | dataSource = DruidDataSourceFactory.createDataSource(map);
309 | }
310 |
311 | public Config getConfig() {
312 | return config;
313 | }
314 |
315 | }
316 |
--------------------------------------------------------------------------------
/odar-mysql-connector/src/main/java/io/openmessaging/mysql/binlog/Transaction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Licensed to the Apache Software Foundation (ASF) under one or more
3 | * contributor license agreements. See the NOTICE file distributed with
4 | * this work for additional information regarding copyright ownership.
5 | * The ASF licenses this file to You under the Apache License, Version 2.0
6 | * (the "License"); you may not use this file except in compliance with
7 | * the License. You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | package io.openmessaging.mysql.binlog;
19 |
20 | import com.alibaba.fastjson.JSONObject;
21 | import io.openmessaging.connector.api.data.EntryType;
22 | import io.openmessaging.mysql.Config;
23 | import io.openmessaging.mysql.position.BinlogPosition;
24 | import io.openmessaging.mysql.schema.Table;
25 | import java.io.Serializable;
26 | import java.util.HashMap;
27 | import java.util.LinkedList;
28 | import java.util.List;
29 | import java.util.Map;
30 |
31 | public class Transaction {
32 |
33 | private BinlogPosition nextBinlogPosition;
34 | private Long xid;
35 |
36 | private Config config;
37 |
38 | private List list = new LinkedList<>();
39 |
40 | public Transaction(Config config) {
41 | this.config = config;
42 | }
43 |
44 | public boolean addRow(EntryType type, Table table, Serializable[] row, Serializable[] rowBeforeUpdate) {
45 |
46 | if (list.size() == config.maxTransactionRows) {
47 | return false;
48 | } else {
49 | DataRow dataRow = new DataRow(type, table, row, rowBeforeUpdate);
50 | list.add(dataRow);
51 | return true;
52 | }
53 |
54 | }
55 |
56 | public List getDataRows(){
57 | return list;
58 | }
59 |
60 | public String toJson() {
61 |
62 | List