diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..35b8d1d5 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,61 @@ +version: 2.1 + +executors: + jdk11: + docker: + +jobs: + build: + docker: + - image: cimg/openjdk:11.0 + steps: + - checkout + - restore_cache: + key: dependency-cache-{{ checksum "pom.xml" }} + - run: + name: Cache m2 artifacts + command: mvn dependency:go-offline + - save_cache: + key: dependency-cache-{{ checksum "pom.xml" }} + paths: [ "~/.m2" ] + test: + docker: + - image: cimg/openjdk:11.0 + parameters: + mysql: + type: string + environment: + MYSQL_VERSION: "<< parameters.mysql >>" + JAVA_TOOL_OPTIONS: "-Xmx250m -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn" + steps: + - checkout + - restore_cache: + key: dependency-cache-{{ checksum "pom.xml" }} + - run: + name: "testing under version << parameters.mysql >>" + command: mvn verify -Dgpg.skip + - store_artifacts: + path: test.log + +workflows: + version: 2 + build_and_test: + jobs: + - build + - test: + name: "test-5.5" + mysql: "5.5" + requires: [ "build" ] + - test: + name: "test-5.7" + mysql: "5.7" + requires: [ "build" ] + - test: + name: "test-8.0" + mysql: "8.0" + requires: [ "build" ] + - test: + name: "test-mariadb" + mysql: "mariadb" + requires: [ "build" ] + diff --git a/.github/workflows/mysql-57.yml b/.github/workflows/mysql-57.yml new file mode 100644 index 00000000..46b090a9 --- /dev/null +++ b/.github/workflows/mysql-57.yml @@ -0,0 +1,45 @@ +# +# Copyright Gunnar Morling +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Build and run tests against MySQL 5.7 + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Cache Maven packages + uses: actions/cache@v1 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Build with Maven + run: mvn -B install --file pom.xml diff --git a/.github/workflows/mysql-80.yml b/.github/workflows/mysql-80.yml new file mode 100644 index 00000000..2787a97c --- /dev/null +++ b/.github/workflows/mysql-80.yml @@ -0,0 +1,45 @@ +# +# Copyright Gunnar Morling +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Build and run tests against MySQL 8.0 + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Cache Maven packages + uses: actions/cache@v1 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Build with Maven + run: MYSQL_VERSION=8.0 mvn -B install --file pom.xml diff --git a/.gitignore b/.gitignore index 38431278..9c209dfb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.idea +*.idea *.iml .DS_Store target @@ -6,3 +6,5 @@ target .project .settings .vagrant +.*.sw* +target/ diff --git a/.mvn/maven.config b/.mvn/maven.config deleted file mode 100644 index ccb1b15d..00000000 --- a/.mvn/maven.config +++ /dev/null @@ -1 +0,0 @@ --s .mvn/settings.xml diff --git a/.mvn/settings.xml b/.mvn/settings.xml deleted file mode 100644 index e25acb74..00000000 --- a/.mvn/settings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - maven-central - ${env.OSS_SONATYPE_ORG_USERNAME} - ${env.OSS_SONATYPE_ORG_PASSWORD} - - - - diff --git a/CHANGELOG.md b/CHANGELOG.md index fd3c07ac..c845d140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,108 @@ # Changelog -All notable changes to this project will be documented in this file. -This project adheres to [Semantic Versioning](http://semver.org/). +## [0.27.5](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.27.5...0.27.4) - 2022-11-01 + +- add mariadb BINLOG_CHECKPOINT event + +## [0.27.4](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.27.4...0.27.3) - 2022-11-01 + +- move mariadb_slave_capability back to 4 + +## [0.27.3](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.27.3...0.27.2) - 2022-09-25 + +- pass use-annotate-rows through non-gtid mariadb connections + +# Changelog +## [0.27.2](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.27.2...0.27.1) - 2022-09-16 + +- Fix the maria gtid detection regex to avoid erroneously detecting mysql gtids as maria + +## [0.27.1](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.27.0...0.27.1) - 2022-08-28 + +- fix a bug around the capability that we send maria + +## [0.27.0](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.26.1...0.27.0) - 2022-08-27 + +- Add "official" MariaDB support. @wingerx started this worked and @ivapiv bugged me until it was + done, thanks all. This includes: +- MariaDB GTID support +- support for the ANNOTATE_ROWS_EVENTS +- MariaDB detection in getMariaDB() + +## [0.26.1](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.26.0...0.26.1) - 2022-07-18 + +- fix deadlock with disconnect and keepalive thread. + +## [0.26.0](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.25.6...0.26.0) - 2022-07-15 + +- Compressed binlogs, thank you Somesh Malviya +- fix crash on unknown field type + +# Changelog + +## [0.25.6](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.25.5...0.25.6) - 2022-04-14 + +- stop crashing in an inopportune place + +## [0.25.5](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.25.4...0.25.5) - 2022-01-28 + +- mysql 8 also puts JSON keys in any damn place it likes + +## [0.25.4](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.25.3...0.25.4) - 2021-10-13 + +- add debugging info to eof exception + +## [0.25.3](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.25.2...0.25.3) - 2021-07-29 + +- support mysql 8's invisible columns + +## [0.25.2](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.25.1...0.25.2) - 2021-06-25 + +- allow `setupConnection()` to be overridden +- upgrade to TLS v1.2 + +## [0.25.1](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.25.0...0.25.1) - 2021-04-20 + +- performance improves in ByteArrayInputStream#read + +## [0.25.0](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.24.1...0.25.0) - 2021-03-04 + +- bring back jdk 8 support, this caused... ahem. Issues. + +## [0.24.1](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.24.0...0.24.1) - 2021-03-03 + +- Fix for performance issues read JSON columns + +## [0.24.0](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.23.3...0.24.0) - 2021-02-04 + +- Move up to JDK 11, drop support for JDK 8 + +## [0.23.4](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.23.3...0.23.4) - 2021-01-17 + +- correct authentication error that was causing a problem with Azure + +## [0.23.3](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.23.2...0.23.3) - 2020-10-29 + +- add EventDeserializer.CompatibilityMode.INTEGER_AS_BYTE_ARRAY if you want raw integer data +- don't crash on AWS Aurora's unknown event types + +## [0.23.2](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.23.1...0.23.2) - 2020-07-25 + +- `connect` now throws `IllegalStateException` when already connected + +## [0.23.1](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.22.2...0.23.1) - 2020-05-25 + +- this releases adds support for mysql 8's `caching_sha2_password` authentication method + +## [0.22.2](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.22.0...0.22.2) - 2020-04-29 + +- Fix bugs in 0.22.0 involving nested JSON objects. + +## [0.22.0](https://github.com/osheroff/mysql-binlog-connector-java/compare/0.20.1...0.22.0) - 2020-04-24 + +- *THIS RELEASE IS BUGGY. DO NOT USE.* +- master server id is exposed in the library https://github.com/shyiko/mysql-binlog-connector-java/pull/319 +- Fixes for JSON data in mysql 8.0.16+ https://github.com/shyiko/mysql-binlog-connector-java/pull/288 +- more fixes for the bizarre azure platform https://github.com/shyiko/mysql-binlog-connector-java/pull/275 ## [0.20.1](https://github.com/shyiko/mysql-binlog-connector-java/compare/0.20.0...0.20.1) - 2019-05-12 @@ -97,7 +199,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [0.10.1](https://github.com/shyiko/mysql-binlog-connector-java/compare/0.10.0...0.10.1) - 2017-02-28 ### Fixed -- HEARTBEAT tracking ([118](https://github.com/shyiko/mysql-binlog-connector-java/issues/118#issuecomment-283138143)). +- HEARTBEAT tracking ([118](https://github.com/shyiko/mysql-binlog-connector-java/issues/118#issuecomment-283138143)). ## [0.10.0](https://github.com/shyiko/mysql-binlog-connector-java/compare/0.9.2...0.10.0) - 2017-02-28 @@ -119,13 +221,13 @@ isn't reached within `BinaryLogClient::connectTimeout` from `BinaryLogClient::co ### Fixed - - NPE in case of EOF (BinaryLogClient) ([153](https://github.com/shyiko/mysql-binlog-connector-java/pull/153)). + - NPE in case of EOF (BinaryLogClient) ([153](https://github.com/shyiko/mysql-binlog-connector-java/pull/153)). ## [0.9.0](https://github.com/shyiko/mysql-binlog-connector-java/compare/0.8.1...0.9.0) - 2017-02-07 ### Added - - `BinaryLogClient::connectTimeout` (3 seconds by default). + - `BinaryLogClient::connectTimeout` (3 seconds by default). NOTE: `BinaryLogClient::keepAliveConnectTimeout` has been deprecated and is going to be removed in 1.0.0. ## [0.8.1](https://github.com/shyiko/mysql-binlog-connector-java/compare/0.8.0...0.8.1) - 2016-01-10 @@ -144,14 +246,14 @@ isn't reached within `BinaryLogClient::connectTimeout` from `BinaryLogClient::co ### Fixed - - `SSLMode.PREFERRED` handling (verification against the CA is no longer enforced) ([#142](https://github.com/shyiko/mysql-binlog-connector-java/pull/142)). + - `SSLMode.PREFERRED` handling (verification against the CA is no longer enforced) ([#142](https://github.com/shyiko/mysql-binlog-connector-java/pull/142)). NOTE: This change does NOT affect `SSLMode.VERIFY_CA` / `SSLMode.VERIFY_IDENTITY`. ## [0.7.3](https://github.com/shyiko/mysql-binlog-connector-java/compare/0.7.2...0.7.3) - 2016-12-26 ### Fixed - - Handling of DATE/DATETIME/TIMESTAMP "zero" value (e.g. '0000-00-00') when + - Handling of DATE/DATETIME/TIMESTAMP "zero" value (e.g. '0000-00-00') when `CompatibilityMode.DATE_AND_TIME_AS_LONG_MICRO` is set (false by default). ## [0.7.2](https://github.com/shyiko/mysql-binlog-connector-java/compare/0.7.1...0.7.2) - 2016-12-26 @@ -173,7 +275,7 @@ isn't reached within `BinaryLogClient::connectTimeout` from `BinaryLogClient::co ## [0.6.0](https://github.com/shyiko/mysql-binlog-connector-java/compare/0.5.2...0.6.0) - 2016-11-27 -### Added +### Added - `EventDeserializer` compatibility modes to mimic upcoming 1.0.0 event deserialization behavior ([#131](https://github.com/shyiko/mysql-binlog-connector-java/pull/131)). ## [0.5.2](https://github.com/shyiko/mysql-binlog-connector-java/compare/0.5.1...0.5.2) - 2016-11-19 @@ -202,9 +304,9 @@ isn't reached within `BinaryLogClient::connectTimeout` from `BinaryLogClient::co ### Fixed - GTID "rollover". - - binlog position tracking (`binaryLogClient.binlogPosition` is no longer updated on TABLE_MAP so that in case of - reconnect (using a different instance of client) table mapping (used by *RowsEventDataDeserializer|s) could be - reconstructed before hitting *RowsEvent. + - binlog position tracking (`binaryLogClient.binlogPosition` is no longer updated on TABLE_MAP so that in case of + reconnect (using a different instance of client) table mapping (used by *RowsEventDataDeserializer|s) could be + reconstructed before hitting *RowsEvent. ## [0.4.0](https://github.com/shyiko/mysql-binlog-connector-java/compare/0.3.3...0.4.0) - 2016-08-15 @@ -242,7 +344,7 @@ isn't reached within `BinaryLogClient::connectTimeout` from `BinaryLogClient::co ### Fixed - Possible infinite loop in case of EOF in the middle of `ByteArrayInputStream::fill`. - + ## [0.2.3](https://github.com/shyiko/mysql-binlog-connector-java/compare/0.2.2...0.2.3) - 2015-08-31 ### Fixed @@ -269,8 +371,8 @@ isn't reached within `BinaryLogClient::connectTimeout` from `BinaryLogClient::co - Support for authentication via empty password ([#39](https://github.com/shyiko/mysql-binlog-connector-java/issues/39)). ### Changed -- Server error reporting ([#37](https://github.com/shyiko/mysql-binlog-connector-java/issues/37)). - WARNING: If you are using exception message to identify specific server errors - you'll need to switch to +- Server error reporting ([#37](https://github.com/shyiko/mysql-binlog-connector-java/issues/37)). + WARNING: If you are using exception message to identify specific server errors - you'll need to switch to `ServerException`::[errorCode](https://github.com/shyiko/mysql-binlog-connector-java/commit/1817d0ff709c65c31af9236dcc4e50cc3ad1023b#diff-0dff747d57cb3f5f0548be89a81e29f8R37) (as message no longer includes error code). ### Fixed diff --git a/README.md b/README.md index 595feb03..761d17d3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,20 @@ -# mysql-binlog-connector-java [![Build Status](https://travis-ci.org/shyiko/mysql-binlog-connector-java.svg?branch=master)](https://travis-ci.org/shyiko/mysql-binlog-connector-java) [![Coverage Status](https://coveralls.io/repos/shyiko/mysql-binlog-connector-java/badge.svg?branch=master)](https://coveralls.io/r/shyiko/mysql-binlog-connector-java?branch=master) [![Maven Central](https://img.shields.io/maven-central/v/com.github.shyiko/mysql-binlog-connector-java.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.github.shyiko%22%20AND%20a%3A%22mysql-binlog-connector-java%22) +# mysql-binlog-connector-java [![Build Status](https://travis-ci.org/shyiko/mysql-binlog-connector-java.svg?branch=master)](https://travis-ci.org/shyiko/mysql-binlog-connector-java) [![Coverage Status](https://coveralls.io/repos/shyiko/mysql-binlog-connector-java/badge.svg?branch=master)](https://coveralls.io/r/shyiko/mysql-binlog-connector-java?branch=master) [![Maven Central](https://img.shields.io/maven-central/v/com.zendesk/mysql-binlog-connector-java.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.github.shyiko%22%20AND%20a%3A%22mysql-binlog-connector-java%22) -MySQL Binary Log connector. -Initially project was started as a fork of [open-replicator](https://code.google.com/p/open-replicator), +MySQL Binary Log connector. @osheroff's fork of @shiyko's project, probably +the "official" version of this. With help from the Debezium devs. + +## Usage + +```xml + + com.zendesk + mysql-binlog-connector-java + 0.25.0 + +``` + +Initially project was started as a fork of [open-replicator](https://code.google.com/p/open-replicator), but ended up as a complete rewrite. Key differences/features: - automatic binlog filename/position | GTID resolution @@ -16,21 +28,12 @@ but ended up as a complete rewrite. Key differences/features: - no third-party dependencies - test suite over different versions of MySQL releases -> If you are looking for something similar in other languages - check out -[siddontang/go-mysql](https://github.com/siddontang/go-mysql) (Go), +> If you are looking for something similar in other languages - check out +[siddontang/go-mysql](https://github.com/siddontang/go-mysql) (Go), [noplay/python-mysql-replication](https://github.com/noplay/python-mysql-replication) (Python). -## Usage -Get the latest JAR(s) from [here](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.github.shyiko%22%20AND%20a%3A%22mysql-binlog-connector-java%22). Alternatively you can include following Maven dependency (available through Maven Central): - -```xml - - com.github.shyiko - mysql-binlog-connector-java - 0.18.1 - -``` +Or get the latest JAR(s) from [here](https://search.maven.org/search?q=g:com.zendesk%20AND%20a:mysql-binlog-connector-java). #### Reading binary log file @@ -78,30 +81,40 @@ client.connect(); > By default, BinaryLogClient starts from the current (at the time of connect) master binlog position. If you wish to kick off from a specific filename or position, use `client.setBinlogFilename(filename)` + `client.setBinlogPosition(position)`. -> `client.connect()` is blocking (meaning that client will listen for events in the current thread). -`client.connect(timeout)`, on the other hand, spawns a separate thread. +> `client.connect()` is blocking (meaning that client will listen for events in the current thread). +`client.connect(timeout)`, on the other hand, spawns a separate thread. + + +#### MariaDB + +The stock BinaryLogClient works out of the box with MariaDB but there's two differences; + +One, MariaDB's GTIDs are different. They're still strings but parse differently. +Two, Maria can send the ANNOTATE_ROWS events which allows you to recover the SQL used to generate rows in row-based replication. + +See https://mariadb.com/kb/en/annotate_rows_log_event/ and `client.setUseSendAnnotateRowsEvent(true)` #### Controlling event deserialization -> You might need it for several reasons: -you don't want to waste time deserializing events you won't need; -there is no EventDataDeserializer defined for the event type you are interested in (or there is but it contains a bug); -you want certain type of events to be deserialized in a different way (perhaps *RowsEventData should contain table +> You might need it for several reasons: +you don't want to waste time deserializing events you won't need; +there is no EventDataDeserializer defined for the event type you are interested in (or there is but it contains a bug); +you want certain type of events to be deserialized in a different way (perhaps *RowsEventData should contain table name and not id?); etc. ```java EventDeserializer eventDeserializer = new EventDeserializer(); // do not deserialize EXT_DELETE_ROWS event data, return it as a byte array -eventDeserializer.setEventDataDeserializer(EventType.EXT_DELETE_ROWS, - new ByteArrayEventDataDeserializer()); +eventDeserializer.setEventDataDeserializer(EventType.EXT_DELETE_ROWS, + new ByteArrayEventDataDeserializer()); // skip EXT_WRITE_ROWS event data altogether -eventDeserializer.setEventDataDeserializer(EventType.EXT_WRITE_ROWS, +eventDeserializer.setEventDataDeserializer(EventType.EXT_WRITE_ROWS, new NullEventDataDeserializer()); // use custom event data deserializer for EXT_DELETE_ROWS -eventDeserializer.setEventDataDeserializer(EventType.EXT_DELETE_ROWS, +eventDeserializer.setEventDataDeserializer(EventType.EXT_DELETE_ROWS, new EventDataDeserializer() { ... }); @@ -119,7 +132,7 @@ BinaryLogClient binaryLogClient = ... ObjectName objectName = new ObjectName("mysql.binlog:type=BinaryLogClient"); mBeanServer.registerMBean(binaryLogClient, objectName); -// following bean accumulates various BinaryLogClient stats +// following bean accumulates various BinaryLogClient stats // (e.g. number of disconnects, skipped events) BinaryLogClientStatistics stats = new BinaryLogClientStatistics(binaryLogClient); ObjectName statsObjectName = new ObjectName("mysql.binlog:type=BinaryLogClientStatistics"); @@ -130,12 +143,12 @@ mBeanServer.registerMBean(stats, statsObjectName); > Introduced in 0.4.0. -TLSv1.1 & TLSv1.2 require [JDK 7](http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6916074)+. -Prior to MySQL 5.7.10, MySQL supported only TLSv1 -(see [Secure Connection Protocols and Ciphers](http://dev.mysql.com/doc/refman/5.7/en/secure-connection-protocols-ciphers.html)). +TLSv1.1 & TLSv1.2 require [JDK 7](http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6916074)+. +Prior to MySQL 5.7.10, MySQL supported only TLSv1 +(see [Secure Connection Protocols and Ciphers](http://dev.mysql.com/doc/refman/5.7/en/secure-connection-protocols-ciphers.html)). > To check that MySQL server is [properly configured with SSL support](http://dev.mysql.com/doc/refman/5.7/en/using-secure-connections.html) - -`mysql -h host -u root -ptypeyourpasswordmaybe -e "show global variables like 'have_%ssl';"` ("Value" +`mysql -h host -u root -ptypeyourpasswordmaybe -e "show global variables like 'have_%ssl';"` ("Value" should be "YES"). State of the current session can be determined using `\s` ("SSL" should not be blank). ```java @@ -151,23 +164,23 @@ client.setSSLMode(SSLMode.VERIFY_IDENTITY); ## Implementation notes - data of numeric types (tinyint, etc) always returned signed(!) regardless of whether column definition includes "unsigned" keyword or not. -- data of var\*/\*text/\*blob types always returned as a byte array (for var\* this is true starting from 1.0.0). +- data of var\*/\*text/\*blob types always returned as a byte array (for var\* this is true starting from 1.0.0). ## Frequently Asked Questions **Q**. How does a typical transaction look like? - -**A**. GTID event (if gtid_mode=ON) -> QUERY event with "BEGIN" as sql -> ... -> XID event | QUERY event with "COMMIT" or "ROLLBACK" as sql. -**Q**. EventData for inserted/updated/deleted rows has no information about table (except for some weird id). -How do I make sense out of it? +**A**. GTID event (if gtid_mode=ON) -> QUERY event with "BEGIN" as sql -> ... -> XID event | QUERY event with "COMMIT" or "ROLLBACK" as sql. + +**Q**. EventData for inserted/updated/deleted rows has no information about table (except for some weird id). +How do I make sense out of it? **A**. Each [WriteRowsEventData](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/WriteRowsEventData.java)/[UpdateRowsEventData](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/UpdateRowsEventData.java)/[DeleteRowsEventData](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/DeleteRowsEventData.java) event is preceded by [TableMapEventData](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/TableMapEventData.java) which contains schema & table name. If for some reason you need to know column names (types, etc). - the easiest way is to ```sql -select TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, -DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, CHARACTER_OCTET_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE, +select TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, +DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, CHARACTER_OCTET_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE, CHARACTER_SET_NAME, COLLATION_NAME from INFORMATION_SCHEMA.COLUMNS; # see https://dev.mysql.com/doc/refman/5.6/en/columns-table.html for more information ``` @@ -180,10 +193,10 @@ You can find JDBC snippet [here](https://github.com/shyiko/mysql-binlog-connecto #### API overview -There are two entry points - [BinaryLogClient](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogClient.java) (which you can use to read binary logs from a MySQL server) and -[BinaryLogFileReader](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogFileReader.java) (for offline log processing). Both of them rely on [EventDeserializer](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventDeserializer.java) to deserialize -stream of events. Each [Event](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/Event.java) consists of [EventHeader](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/EventHeader.java) (containing among other things reference to [EventType](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/EventType.java)) and -[EventData](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/EventData.java). The aforementioned EventDeserializer has one [EventHeaderDeserializer](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventHeaderDeserializer.java) ([EventHeaderV4Deserializer](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventHeaderV4Deserializer.java) by default) +There are two entry points - [BinaryLogClient](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogClient.java) (which you can use to read binary logs from a MySQL server) and +[BinaryLogFileReader](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogFileReader.java) (for offline log processing). Both of them rely on [EventDeserializer](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventDeserializer.java) to deserialize +stream of events. Each [Event](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/Event.java) consists of [EventHeader](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/EventHeader.java) (containing among other things reference to [EventType](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/EventType.java)) and +[EventData](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/EventData.java). The aforementioned EventDeserializer has one [EventHeaderDeserializer](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventHeaderDeserializer.java) ([EventHeaderV4Deserializer](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventHeaderV4Deserializer.java) by default) and [a collection of EventDataDeserializer|s](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventDeserializer.java#L82). If there is no EventDataDeserializer registered for some particular type of Event - default EventDataDeserializer kicks in ([NullEventDataDeserializer](https://github.com/shyiko/mysql-binlog-connector-java/blob/master/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/NullEventDataDeserializer.java)). @@ -193,9 +206,10 @@ For the insight into the internals of MySQL look [here](https://dev.mysql.com/do ## Real-world applications -Some of the OSS using / built on top of mysql-binlog-conector-java: +Some of the OSS using / built on top of mysql-binlog-conector-java: * [apache/nifi](https://github.com/apache/nifi) An easy to use, powerful, and reliable system to process and distribute data. * [debezium](https://github.com/debezium/debezium) A low latency data streaming platform for change data capture (CDC). +* [zendesk/maxwell](https://github.com/zendesk/maxwell) A MySQL-to-JSON Kafka producer. * [mavenlink/changestream](https://github.com/mavenlink/changestream) - A stream of changes for MySQL built on Akka. * [mardambey/mypipe](https://github.com/mardambey/mypipe) MySQL binary log consumer with the ability to act on changed rows and publish changes to different systems with emphasis on Apache Kafka. * [ngocdaothanh/mydit](https://github.com/ngocdaothanh/mydit) MySQL to MongoDB data replicator. @@ -203,10 +217,9 @@ Some of the OSS using / built on top of mysql-binlog-conector-java: * [shyiko/rook](https://github.com/shyiko/rook) Generic Change Data Capture (CDC) toolkit. * [streamsets/datacollector](https://github.com/streamsets/datacollector) Continuous big data ingestion infrastructure. * [twingly/ecco](https://github.com/twingly/ecco) MySQL replication binlog parser in JRuby. -* [zendesk/maxwell](https://github.com/zendesk/maxwell) A MySQL-to-JSON Kafka producer. * [zzt93/syncer](https://github.com/zzt93/syncer) A tool sync & manipulate data from MySQL/MongoDB to ES/Kafka/MySQL, which make 'Eventual Consistency' promise. -It's also used [on a large scale](https://twitter.com/atwinmutt/status/626816601078300672) in MailChimp. You can read about it [here](http://devs.mailchimp.com/blog/powering-mailchimp-pro-reporting/). +It's also used [on a large scale](https://twitter.com/atwinmutt/status/626816601078300672) in MailChimp. You can read about it [here](http://devs.mailchimp.com/blog/powering-mailchimp-pro-reporting/). ## Development @@ -216,12 +229,20 @@ cd mysql-binlog-connector-java mvn # shows how to build, test, etc. project ``` +## Deployment + +setup your settings.xml to have a "central" entry. + +``` +mvn deploy +``` + ## Contributing -In lieu of a formal styleguide, please take care to maintain the existing coding style. -Executing `mvn checkstyle:check` within project directory should not produce any errors. +In lieu of a formal styleguide, please take care to maintain the existing coding style. +Executing `mvn checkstyle:check` within project directory should not produce any errors. If you are willing to install [vagrant](http://www.vagrantup.com/) (required by integration tests) it's highly recommended -to check (with `mvn clean verify`) that there are no test failures before sending a pull request. +to check (with `mvn clean verify`) that there are no test failures before sending a pull request. Additional tests for any new or changed functionality are also very welcomed. ## License diff --git a/pom.xml b/pom.xml index a3f78dcb..d0665633 100644 --- a/pom.xml +++ b/pom.xml @@ -2,13 +2,13 @@ 4.0.0 - com.github.shyiko + com.zendesk mysql-binlog-connector-java - 0.0.0-SNAPSHOT + 0.7.3-4-FIVETRAN mysql-binlog-connector-java MySQL Binary Log connector - https://github.com/shyiko/mysql-binlog-connector-java + https://github.com/osheroff/mysql-binlog-connector-java Apache License, Version 2.0 @@ -17,9 +17,9 @@ - scm:git:git@github.com:shyiko/mysql-binlog-connector-java.git - scm:git:git@github.com:shyiko/mysql-binlog-connector-java.git - git@github.com:shyiko/mysql-binlog-connector-java.git + scm:git:git@github.com:osheroff/mysql-binlog-connector-java.git + scm:git:git@github.com:osheroff/mysql-binlog-connector-java.git + git@github.com:osheroff/mysql-binlog-connector-java.git @@ -27,15 +27,12 @@ stanley.shyiko@gmail.com Stanley Shyiko + + osheroff + ben@gimbo.net + Ben Osheroff + - - - maven-central - Sonatype Nexus Staging - https://oss.sonatype.org/service/local/staging/deploy/maven2 - - - UTF-8 UTF-8 @@ -68,28 +65,46 @@ 8.0.15 test + + com.fasterxml.jackson.core + jackson-core + 2.13.4 + test + + + com.fasterxml.jackson.core + jackson-databind + 2.13.4.2 + test + + + com.github.luben + zstd-jni + 1.5.0-2 + compile + - - - - - - org.apache.maven.plugins - maven-jxr-plugin - 2.3 - - - - + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.6 + true + + central + https://oss.sonatype.org/ + true + 10 + true + + org.apache.maven.plugins maven-compiler-plugin - 3.5.1 + 3.8.1 - 1.6 - 1.6 + 8 @@ -105,14 +120,6 @@ - - org.apache.maven.plugins - maven-deploy-plugin - 2.8.2 - - true - - org.apache.maven.plugins maven-surefire-plugin @@ -141,119 +148,56 @@ - - org.codehaus.mojo - exec-maven-plugin - 1.1.1 + org.apache.maven.plugins + maven-source-plugin + 3.2.1 - start-vagrant-vm - pre-integration-test + attach-sources + verify - exec + jar-no-fork - - ${vagrant.integration.box} - ${vagrant.bin} - - up - - ${skipTests} - + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.4.0 + - destroy-vagrant-vm - post-integration-test + attach-javadocs - exec + jar - - ${vagrant.integration.box} - ${vagrant.bin} - - destroy - --force - - ${skipTests} - org.apache.maven.plugins - maven-checkstyle-plugin - 2.9.1 - - true - supplement/codequality/checkstyle.xml - supplement/codequality/license.header - true - true - + maven-gpg-plugin + 1.6 + sign-artifacts verify - checkstyle + sign - - - com.github.shyiko - checkstyle-nonstandard - 0.1.0 - - - - - com.github.shyiko.usage-maven-plugin - usage-maven-plugin - 1.0.0 - - - # build everything (append "-DskipTests=true" if you wish to skip tests) - ./mvnw clean package - - # run unit + integration tests, validate codebase using checkstyle - ./mvnw -P coverage clean verify - # use -Dvagrant.integration.box= to switch between MySQL sandboxes - - # for aggregated coverage over different mysql releases use - ./mvnw clean - ./mvnw -P coverage verify \ - -Dvagrant.integration.box=supplement/vagrant/mysql-5.5.27-sandbox-prepackaged - ./mvnw -P coverage verify \ - -Dvagrant.integration.box=supplement/vagrant/mysql-5.6.12-sandbox-prepackaged - ./mvnw -P coverage verify \ - -Dvagrant.integration.box=supplement/vagrant/mysql-5.7.15-sandbox-prepackaged - ./mvnw -P coverage,mysql-8-compat verify \ - -Dvagrant.integration.box=supplement/vagrant/mysql-8.0.1-sandbox-prepackaged - - # submit coverage report to coveralls - ./mvnw -P coverage coveralls:jacoco -DrepoToken=<coveralls.io> - - # publish a new version - ./mvnw versions:set -DnewVersion=<version> - ./mvnw -Ddeploy=maven-central - git tag <version> && git push origin <version> - - - com.github.shyiko.usage-maven-plugin - usage-maven-plugin - 1.0.0 + org.springframework.build + aws-maven + 5.0.0.RELEASE - mysql-8-compat @@ -266,117 +210,22 @@ - - coverage - - - - org.jacoco - jacoco-maven-plugin - 0.7.9 - - ${basedir}/target/coverage-reports/jacoco-unit.exec - ${basedir}/target/coverage-reports/jacoco-unit.exec - true - - **/ClientCapabilities.* - - - - - jacoco-initialize - - prepare-agent - - - - jacoco-site - post-integration-test - - report - - - - - - org.eluder.coveralls - coveralls-maven-plugin - 2.0.0 - - - - - - deploy-to-maven-central - - - deploy - maven-central - - - - clean deploy - - - org.apache.maven.plugins - maven-source-plugin - 3.0.1 - - - attach-sources - verify - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.10.4 - - - attach-javadocs - - jar - - - -Xdoclint:none - true - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - sign-artifacts - verify - - sign - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.7 - true - - https://oss.sonatype.org/ - maven-central - true - - - - - - + + + + fivetran-maven-release + s3://fivetran-maven/public/release + + + + + + aws-public-release + AWS Public Release Repository + https://s3.amazonaws.com/fivetran-maven/public/release + + + diff --git a/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogClient.java b/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogClient.java index c395aa7b..43bd362a 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogClient.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogClient.java @@ -15,48 +15,48 @@ */ package com.github.shyiko.mysql.binlog; +import com.github.shyiko.mysql.binlog.event.AnnotateRowsEventData; import com.github.shyiko.mysql.binlog.event.Event; import com.github.shyiko.mysql.binlog.event.EventHeader; import com.github.shyiko.mysql.binlog.event.EventHeaderV4; import com.github.shyiko.mysql.binlog.event.EventType; import com.github.shyiko.mysql.binlog.event.GtidEventData; +import com.github.shyiko.mysql.binlog.event.MariadbGtidEventData; +import com.github.shyiko.mysql.binlog.event.MariadbGtidListEventData; import com.github.shyiko.mysql.binlog.event.QueryEventData; import com.github.shyiko.mysql.binlog.event.RotateEventData; +import com.github.shyiko.mysql.binlog.event.deserialization.AnnotateRowsEventDataDeserializer; import com.github.shyiko.mysql.binlog.event.deserialization.ChecksumType; import com.github.shyiko.mysql.binlog.event.deserialization.EventDataDeserializationException; import com.github.shyiko.mysql.binlog.event.deserialization.EventDataDeserializer; import com.github.shyiko.mysql.binlog.event.deserialization.EventDeserializer; import com.github.shyiko.mysql.binlog.event.deserialization.EventDeserializer.EventDataWrapper; import com.github.shyiko.mysql.binlog.event.deserialization.GtidEventDataDeserializer; +import com.github.shyiko.mysql.binlog.event.deserialization.MariadbGtidEventDataDeserializer; +import com.github.shyiko.mysql.binlog.event.deserialization.MariadbGtidListEventDataDeserializer; import com.github.shyiko.mysql.binlog.event.deserialization.QueryEventDataDeserializer; import com.github.shyiko.mysql.binlog.event.deserialization.RotateEventDataDeserializer; import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; import com.github.shyiko.mysql.binlog.jmx.BinaryLogClientMXBean; import com.github.shyiko.mysql.binlog.network.AuthenticationException; +import com.github.shyiko.mysql.binlog.network.Authenticator; import com.github.shyiko.mysql.binlog.network.ClientCapabilities; import com.github.shyiko.mysql.binlog.network.DefaultSSLSocketFactory; import com.github.shyiko.mysql.binlog.network.SSLMode; import com.github.shyiko.mysql.binlog.network.SSLSocketFactory; import com.github.shyiko.mysql.binlog.network.ServerException; import com.github.shyiko.mysql.binlog.network.SocketFactory; -import com.github.shyiko.mysql.binlog.network.TLSHostnameVerifier; import com.github.shyiko.mysql.binlog.network.protocol.ErrorPacket; import com.github.shyiko.mysql.binlog.network.protocol.GreetingPacket; import com.github.shyiko.mysql.binlog.network.protocol.Packet; import com.github.shyiko.mysql.binlog.network.protocol.PacketChannel; import com.github.shyiko.mysql.binlog.network.protocol.ResultSetRowPacket; -import com.github.shyiko.mysql.binlog.network.protocol.command.AuthenticateCommand; -import com.github.shyiko.mysql.binlog.network.protocol.command.AuthenticateNativePasswordCommand; import com.github.shyiko.mysql.binlog.network.protocol.command.Command; import com.github.shyiko.mysql.binlog.network.protocol.command.DumpBinaryLogCommand; import com.github.shyiko.mysql.binlog.network.protocol.command.DumpBinaryLogGtidCommand; import com.github.shyiko.mysql.binlog.network.protocol.command.PingCommand; import com.github.shyiko.mysql.binlog.network.protocol.command.QueryCommand; import com.github.shyiko.mysql.binlog.network.protocol.command.SSLRequestCommand; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; import java.io.EOFException; import java.io.IOException; import java.net.InetSocketAddress; @@ -69,6 +69,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; @@ -82,6 +83,10 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + /** * MySQL replication stream client. @@ -118,7 +123,7 @@ public X509Certificate[] getAcceptedIssuers() { // https://dev.mysql.com/doc/internals/en/sending-more-than-16mbyte.html private static final int MAX_PACKET_LENGTH = 16777215; - private final Logger logger = Logger.getLogger(getClass().getName()); + private final Logger logger = Logger.getLogger("donkey"); private final String hostname; private final int port; @@ -133,23 +138,26 @@ public X509Certificate[] getAcceptedIssuers() { private volatile long connectionId; private SSLMode sslMode = SSLMode.DISABLED; - private GtidSet gtidSet; - private final Object gtidSetAccessLock = new Object(); + protected GtidSet gtidSet; + protected final Object gtidSetAccessLock = new Object(); private boolean gtidSetFallbackToPurged; + private boolean gtidEnabled = false; private boolean useBinlogFilenamePositionInGtidMode; - private String gtid; + protected String gtid; private boolean tx; private EventDeserializer eventDeserializer = new EventDeserializer(); private final List eventListeners = new CopyOnWriteArrayList(); private final List lifecycleListeners = new CopyOnWriteArrayList(); + protected boolean abortRequest = false; private SocketFactory socketFactory; private SSLSocketFactory sslSocketFactory; - private volatile PacketChannel channel; + protected volatile PacketChannel channel; private volatile boolean connected; + private volatile long masterServerId = -1; private ThreadFactory threadFactory; @@ -165,10 +173,16 @@ public X509Certificate[] getAcceptedIssuers() { private final Lock connectLock = new ReentrantLock(); private final Lock keepAliveThreadExecutorLock = new ReentrantLock(); + private boolean useSendAnnotateRowsEvent; + + + private Boolean isMariaDB; /** * Alias for BinaryLogClient("localhost", 3306, <no schema> = null, username, password). * @see BinaryLogClient#BinaryLogClient(String, int, String, String, String) + * @param username login name + * @param password password */ public BinaryLogClient(String username, String password) { this("localhost", 3306, null, username, password); @@ -177,6 +191,9 @@ public BinaryLogClient(String username, String password) { /** * Alias for BinaryLogClient("localhost", 3306, schema, username, password). * @see BinaryLogClient#BinaryLogClient(String, int, String, String, String) + * @param schema database name, nullable + * @param username login name + * @param password password */ public BinaryLogClient(String schema, String username, String password) { this("localhost", 3306, schema, username, password); @@ -185,6 +202,10 @@ public BinaryLogClient(String schema, String username, String password) { /** * Alias for BinaryLogClient(hostname, port, <no schema> = null, username, password). * @see BinaryLogClient#BinaryLogClient(String, int, String, String, String) + * @param hostname mysql server hostname + * @param port mysql server port + * @param username login name + * @param password password */ public BinaryLogClient(String hostname, int port, String username, String password) { this(hostname, port, null, username, password); @@ -206,6 +227,10 @@ public BinaryLogClient(String hostname, int port, String schema, String username this.password = password; } + public String fivetranClientIdentity() { + return "mysql-binlog-connector-java-osheroff"; + } + public boolean isBlocking() { return blocking; } @@ -228,6 +253,10 @@ public void setSSLMode(SSLMode sslMode) { this.sslMode = sslMode; } + public long getMasterServerId() { + return this.masterServerId; + } + /** * @return server id (65535 by default) * @see #setServerId(long) @@ -305,7 +334,7 @@ public String getGtidSet() { } /** - * @param gtidSet GTID set (can be an empty string). + * @param gtidStr GTID set string (can be an empty string). *

NOTE #1: Any value but null will switch BinaryLogClient into a GTID mode (this will also set binlogFilename * to "" (provided it's null) forcing MySQL to send events starting from the oldest known binlog (keep in mind * that connection will fail if gtid_purged is anything but empty (unless @@ -314,17 +343,30 @@ public String getGtidSet() { * @see #getGtidSet() * @see #setGtidSetFallbackToPurged(boolean) */ - public void setGtidSet(String gtidSet) { - if (gtidSet != null && this.binlogFilename == null) { + public void setGtidSet(String gtidStr) { + if ( gtidStr == null ) + return; + + this.gtidEnabled = true; + + if (this.binlogFilename == null) { this.binlogFilename = ""; } + synchronized (gtidSetAccessLock) { - this.gtidSet = gtidSet != null ? new GtidSet(gtidSet) : null; + if ( !gtidStr.equals("") ) { + if ( MariadbGtidSet.isMariaGtidSet(gtidStr) ) { + this.gtidSet = new MariadbGtidSet(gtidStr); + } else { + this.gtidSet = new GtidSet(gtidStr); + } + } } } /** * @see #setGtidSetFallbackToPurged(boolean) + * @return whether gtid_purged is used as a fallback */ public boolean isGtidSetFallbackToPurged() { return gtidSetFallbackToPurged; @@ -340,6 +382,7 @@ public void setGtidSetFallbackToPurged(boolean gtidSetFallbackToPurged) { /** * @see #setUseBinlogFilenamePositionInGtidMode(boolean) + * @return value of useBinlogFilenamePostionInGtidMode */ public boolean isUseBinlogFilenamePositionInGtidMode() { return useBinlogFilenamePositionInGtidMode; @@ -483,13 +526,29 @@ public void setThreadFactory(ThreadFactory threadFactory) { this.threadFactory = threadFactory; } + + /** + * @return true/false depending on whether we've connected to MariaDB. NULL if not connected. + */ + public Boolean getMariaDB() { + return isMariaDB; + } + + public boolean isUseSendAnnotateRowsEvent() { + return useSendAnnotateRowsEvent; + } + + public void setUseSendAnnotateRowsEvent(boolean useSendAnnotateRowsEvent) { + this.useSendAnnotateRowsEvent = useSendAnnotateRowsEvent; + } /** * Connect to the replication stream. Note that this method blocks until disconnected. * @throws AuthenticationException if authentication fails * @throws ServerException if MySQL server responds with an error * @throws IOException if anything goes wrong while trying to connect + * @throws IllegalStateException if binary log client is already connected */ - public void connect() throws IOException { + public void connect() throws IOException, IllegalStateException { if (!connectLock.tryLock()) { throw new IllegalStateException("BinaryLogClient is already connected"); } @@ -512,14 +571,16 @@ public void connect() throws IOException { ". Please make sure it's running.", e); } GreetingPacket greetingPacket = receiveGreeting(); - authenticate(greetingPacket); + + detectMariaDB(greetingPacket); + tryUpgradeToSSL(greetingPacket); + + new Authenticator(greetingPacket, channel, schema, username, password).authenticate(); + channel.authenticationComplete(); + connectionId = greetingPacket.getThreadId(); if ("".equals(binlogFilename)) { - synchronized (gtidSetAccessLock) { - if (gtidSet != null && "".equals(gtidSet.toString()) && gtidSetFallbackToPurged) { - gtidSet = new GtidSet(fetchGtidPurged()); - } - } + setupGtidSet(); } if (binlogFilename == null) { fetchBinlogFilenameAndPosition(); @@ -530,13 +591,7 @@ public void connect() throws IOException { } binlogPosition = 4; } - ChecksumType checksumType = fetchBinlogChecksum(); - if (checksumType != ChecksumType.NONE) { - confirmSupportOfChecksum(checksumType); - } - if (heartbeatInterval > 0) { - enableHeartbeat(); - } + setupConnection(); gtid = null; tx = false; requestBinaryLogStream(); @@ -573,9 +628,8 @@ public void connect() throws IOException { } ensureEventDataDeserializer(EventType.ROTATE, RotateEventDataDeserializer.class); synchronized (gtidSetAccessLock) { - if (gtidSet != null) { - ensureEventDataDeserializer(EventType.GTID, GtidEventDataDeserializer.class); - ensureEventDataDeserializer(EventType.QUERY, QueryEventDataDeserializer.class); + if (this.gtidEnabled) { + ensureGtidEventDataDeserializer(); } } listenForEventPackets(); @@ -589,6 +643,27 @@ public void connect() throws IOException { } } + private void detectMariaDB(GreetingPacket packet) { + String serverVersion = packet.getServerVersion(); + if ( serverVersion == null ) + return; + + this.isMariaDB = serverVersion.toLowerCase().contains("mariadb"); + } + /** + * Apply additional options for connection before requesting binlog stream. + */ + protected void setupConnection() throws IOException { + ChecksumType checksumType = fetchBinlogChecksum(); + if (checksumType != ChecksumType.NONE) { + confirmSupportOfChecksum(checksumType); + } + setMasterServerId(); + if (heartbeatInterval > 0) { + enableHeartbeat(); + } + } + private PacketChannel openChannel() throws IOException { Socket socket = socketFactory != null ? socketFactory.createSocket() : new Socket(); socket.connect(new InetSocketAddress(hostname, port), (int) connectTimeout); @@ -634,33 +709,75 @@ public Object call() throws Exception { }; } - private GreetingPacket receiveGreeting() throws IOException { - byte[] initialHandshakePacket = channel.read(); - if (initialHandshakePacket[0] == (byte) 0xFF /* error */) { - byte[] bytes = Arrays.copyOfRange(initialHandshakePacket, 1, initialHandshakePacket.length); + protected void checkError(byte[] packet) throws IOException { + if (packet[0] == (byte) 0xFF /* error */) { + byte[] bytes = Arrays.copyOfRange(packet, 1, packet.length); ErrorPacket errorPacket = new ErrorPacket(bytes); throw new ServerException(errorPacket.getErrorMessage(), errorPacket.getErrorCode(), - errorPacket.getSqlState()); + errorPacket.getSqlState()); } + } + + private GreetingPacket receiveGreeting() throws IOException { + byte[] initialHandshakePacket = channel.read(); + checkError(initialHandshakePacket); + return new GreetingPacket(initialHandshakePacket); } + private boolean tryUpgradeToSSL(GreetingPacket greetingPacket) throws IOException { + int collation = greetingPacket.getServerCollation(); + + if (sslMode != SSLMode.DISABLED) { + boolean serverSupportsSSL = (greetingPacket.getServerCapabilities() & ClientCapabilities.SSL) != 0; + if (!serverSupportsSSL && (sslMode == SSLMode.REQUIRED || sslMode == SSLMode.VERIFY_CA || + sslMode == SSLMode.VERIFY_IDENTITY)) { + throw new IOException("MySQL server does not support SSL"); + } + if (serverSupportsSSL) { + SSLRequestCommand sslRequestCommand = new SSLRequestCommand(); + sslRequestCommand.setCollation(collation); + channel.write(sslRequestCommand); + SSLSocketFactory sslSocketFactory = + this.sslSocketFactory != null ? + this.sslSocketFactory : + sslMode == SSLMode.REQUIRED || sslMode == SSLMode.PREFERRED ? + DEFAULT_REQUIRED_SSL_MODE_SOCKET_FACTORY : + DEFAULT_VERIFY_CA_SSL_MODE_SOCKET_FACTORY; + channel.upgradeToSSL(sslSocketFactory, null); + logger.info("SSL enabled"); + return true; + } + } + return false; + } + private void enableHeartbeat() throws IOException { channel.write(new QueryCommand("set @master_heartbeat_period=" + heartbeatInterval * 1000000)); byte[] statementResult = channel.read(); - if (statementResult[0] == (byte) 0xFF /* error */) { - byte[] bytes = Arrays.copyOfRange(statementResult, 1, statementResult.length); - ErrorPacket errorPacket = new ErrorPacket(bytes); - throw new ServerException(errorPacket.getErrorMessage(), errorPacket.getErrorCode(), - errorPacket.getSqlState()); + checkError(statementResult); + } + + private void setMasterServerId() throws IOException { + channel.write(new QueryCommand("select @@server_id")); + ResultSetRowPacket[] resultSet = readResultSet(); + if (resultSet.length >= 0) { + this.masterServerId = Long.parseLong(resultSet[0].getValue(0)); } } - private void requestBinaryLogStream() throws IOException { + protected void requestBinaryLogStream() throws IOException { long serverId = blocking ? this.serverId : 0; // http://bugs.mysql.com/bug.php?id=71178 + if ( this.isMariaDB ) + requestBinaryLogStreamMaria(serverId); + else + requestBinaryLogStreamMysql(serverId); + } + + private void requestBinaryLogStreamMysql(long serverId) throws IOException { Command dumpBinaryLogCommand; synchronized (gtidSetAccessLock) { - if (gtidSet != null) { + if (this.gtidEnabled) { dumpBinaryLogCommand = new DumpBinaryLogGtidCommand(serverId, useBinlogFilenamePositionInGtidMode ? binlogFilename : "", useBinlogFilenamePositionInGtidMode ? binlogPosition : 4, @@ -672,7 +789,33 @@ private void requestBinaryLogStream() throws IOException { channel.write(dumpBinaryLogCommand); } - private void ensureEventDataDeserializer(EventType eventType, + protected void requestBinaryLogStreamMaria(long serverId) throws IOException { + Command dumpBinaryLogCommand; + + /* + https://jira.mariadb.org/browse/MDEV-225 + */ + channel.write(new QueryCommand("SET @mariadb_slave_capability=4")); + checkError(channel.read()); + + synchronized (gtidSetAccessLock) { + if (this.gtidEnabled) { + logger.info(gtidSet.toString()); + channel.write(new QueryCommand("SET @slave_connect_state = '" + gtidSet.toString() + "'")); + checkError(channel.read()); + channel.write(new QueryCommand("SET @slave_gtid_strict_mode = 0")); + checkError(channel.read()); + channel.write(new QueryCommand("SET @slave_gtid_ignore_duplicates = 0")); + checkError(channel.read()); + dumpBinaryLogCommand = new DumpBinaryLogCommand(serverId, "", 0L, isUseSendAnnotateRowsEvent()); + } else { + dumpBinaryLogCommand = new DumpBinaryLogCommand(serverId, binlogFilename, binlogPosition, isUseSendAnnotateRowsEvent()); + } + } + channel.write(dumpBinaryLogCommand); + } + + protected void ensureEventDataDeserializer(EventType eventType, Class eventDataDeserializerClass) { EventDataDeserializer eventDataDeserializer = eventDeserializer.getEventDataDeserializer(eventType); if (eventDataDeserializer.getClass() != eventDataDeserializerClass && @@ -689,77 +832,12 @@ private void ensureEventDataDeserializer(EventType eventType, } } - private void authenticate(GreetingPacket greetingPacket) throws IOException { - int collation = greetingPacket.getServerCollation(); - int packetNumber = 1; - - boolean usingSSLSocket = false; - if (sslMode != SSLMode.DISABLED) { - boolean serverSupportsSSL = (greetingPacket.getServerCapabilities() & ClientCapabilities.SSL) != 0; - if (!serverSupportsSSL && (sslMode == SSLMode.REQUIRED || sslMode == SSLMode.VERIFY_CA || - sslMode == SSLMode.VERIFY_IDENTITY)) { - throw new IOException("MySQL server does not support SSL"); - } - if (serverSupportsSSL) { - SSLRequestCommand sslRequestCommand = new SSLRequestCommand(); - sslRequestCommand.setCollation(collation); - channel.write(sslRequestCommand, packetNumber++); - SSLSocketFactory sslSocketFactory = - this.sslSocketFactory != null ? - this.sslSocketFactory : - sslMode == SSLMode.REQUIRED || sslMode == SSLMode.PREFERRED ? - DEFAULT_REQUIRED_SSL_MODE_SOCKET_FACTORY : - DEFAULT_VERIFY_CA_SSL_MODE_SOCKET_FACTORY; - channel.upgradeToSSL(sslSocketFactory, - sslMode == SSLMode.VERIFY_IDENTITY ? new TLSHostnameVerifier() : null); - usingSSLSocket = true; - } - } - AuthenticateCommand authenticateCommand = new AuthenticateCommand(schema, username, password, - greetingPacket.getScramble()); - authenticateCommand.setCollation(collation); - channel.write(authenticateCommand, packetNumber); - byte[] authenticationResult = channel.read(); - if (authenticationResult[0] != (byte) 0x00 /* ok */) { - if (authenticationResult[0] == (byte) 0xFF /* error */) { - byte[] bytes = Arrays.copyOfRange(authenticationResult, 1, authenticationResult.length); - ErrorPacket errorPacket = new ErrorPacket(bytes); - throw new AuthenticationException(errorPacket.getErrorMessage(), errorPacket.getErrorCode(), - errorPacket.getSqlState()); - } else if (authenticationResult[0] == (byte) 0xFE) { - switchAuthentication(authenticationResult, usingSSLSocket); - } else { - throw new AuthenticationException("Unexpected authentication result (" + authenticationResult[0] + ")"); - } - } - } - - private void switchAuthentication(byte[] authenticationResult, boolean usingSSLSocket) throws IOException { - /* - Azure-MySQL likes to tell us to switch authentication methods, even though - we haven't advertised that we support any. It uses this for some-odd - reason to send the real password scramble. - */ - ByteArrayInputStream buffer = new ByteArrayInputStream(authenticationResult); - buffer.read(1); - - String authName = buffer.readZeroTerminatedString(); - if ("mysql_native_password".equals(authName)) { - String scramble = buffer.readZeroTerminatedString(); - - Command switchCommand = new AuthenticateNativePasswordCommand(scramble, password); - channel.write(switchCommand, (usingSSLSocket ? 4 : 3)); - byte[] authResult = channel.read(); - - if (authResult[0] != (byte) 0x00) { - byte[] bytes = Arrays.copyOfRange(authResult, 1, authResult.length); - ErrorPacket errorPacket = new ErrorPacket(bytes); - throw new AuthenticationException(errorPacket.getErrorMessage(), errorPacket.getErrorCode(), - errorPacket.getSqlState()); - } - } else { - throw new AuthenticationException("Unsupported authentication type: " + authName); - } + protected void ensureGtidEventDataDeserializer() { + ensureEventDataDeserializer(EventType.GTID, GtidEventDataDeserializer.class); + ensureEventDataDeserializer(EventType.QUERY, QueryEventDataDeserializer.class); + ensureEventDataDeserializer(EventType.ANNOTATE_ROWS, AnnotateRowsEventDataDeserializer.class); + ensureEventDataDeserializer(EventType.MARIADB_GTID, MariadbGtidEventDataDeserializer.class); + ensureEventDataDeserializer(EventType.MARIADB_GTID_LIST, MariadbGtidListEventDataDeserializer.class); } private void spawnKeepAliveThread() { @@ -783,6 +861,7 @@ public void run() { // expected in case of disconnect } if (threadExecutor.isShutdown()) { + logger.info("threadExecutor is shut down, terminating keepalive thread"); return; } boolean connectionLost = false; @@ -796,17 +875,13 @@ public void run() { } } if (connectionLost) { - if (logger.isLoggable(Level.INFO)) { - logger.info("Trying to restore lost connection to " + hostname + ":" + port); - } + logger.info("Keepalive: Trying to restore lost connection to " + hostname + ":" + port); try { terminateConnect(); connect(connectTimeout); } catch (Exception ce) { - if (logger.isLoggable(Level.WARNING)) { - logger.warning("Failed to restore connection to " + hostname + ":" + port + - ". Next attempt in " + keepAliveInterval + "ms"); - } + logger.warning("keepalive: Failed to restore connection to " + hostname + ":" + port + + ". Next attempt in " + keepAliveInterval + "ms"); } } } @@ -861,6 +936,9 @@ public void run() { } catch (IOException e) { exceptionReference.set(e); countDownLatch.countDown(); // making sure we don't end up waiting whole "timeout" + } catch (Exception e) { + exceptionReference.set(new IOException(e)); // method is asynchronous, catch all exceptions so that they are not lost + countDownLatch.countDown(); // making sure we don't end up waiting whole "timeout" } } }; @@ -902,6 +980,30 @@ private String fetchGtidPurged() throws IOException { return ""; } + protected void setupGtidSet() throws IOException{ + if (!this.gtidEnabled) + return; + + synchronized (gtidSetAccessLock) { + if ( this.isMariaDB ) { + if ( gtidSet == null ) { + gtidSet = new MariadbGtidSet(""); + } else if ( !(gtidSet instanceof MariadbGtidSet) ) { + throw new RuntimeException("Connected to MariaDB but given a mysql GTID set!"); + } + } else { + if ( gtidSet == null && gtidSetFallbackToPurged ) { + gtidSet = new GtidSet(fetchGtidPurged()); + } else if ( gtidSet == null ){ + gtidSet = new GtidSet(""); + } else if ( gtidSet instanceof MariadbGtidSet ) { + throw new RuntimeException("Connected to Mysql but given a MariaDB GTID set!"); + } + } + } + + } + private void fetchBinlogFilenameAndPosition() throws IOException { ResultSetRowPacket[] resultSet; channel.write(new QueryCommand("show master status")); @@ -914,7 +1016,7 @@ private void fetchBinlogFilenameAndPosition() throws IOException { binlogPosition = Long.parseLong(resultSetRow.getValue(1)); } - private ChecksumType fetchBinlogChecksum() throws IOException { + protected ChecksumType fetchBinlogChecksum() throws IOException { channel.write(new QueryCommand("show global variables like 'binlog_checksum'")); ResultSetRowPacket[] resultSet = readResultSet(); if (resultSet.length == 0) { @@ -926,20 +1028,16 @@ private ChecksumType fetchBinlogChecksum() throws IOException { private void confirmSupportOfChecksum(ChecksumType checksumType) throws IOException { channel.write(new QueryCommand("set @master_binlog_checksum= @@global.binlog_checksum")); byte[] statementResult = channel.read(); - if (statementResult[0] == (byte) 0xFF /* error */) { - byte[] bytes = Arrays.copyOfRange(statementResult, 1, statementResult.length); - ErrorPacket errorPacket = new ErrorPacket(bytes); - throw new ServerException(errorPacket.getErrorMessage(), errorPacket.getErrorCode(), - errorPacket.getSqlState()); - } + checkError(statementResult); eventDeserializer.setChecksumType(checksumType); } - private void listenForEventPackets() throws IOException { + protected void listenForEventPackets() throws IOException { + abortRequest = false; ByteArrayInputStream inputStream = channel.getInputStream(); boolean completeShutdown = false; try { - while (inputStream.peek() != -1) { + while (!abortRequest && inputStream.peek() != -1) { int packetLength = inputStream.readInteger(3); inputStream.skip(1); // 1 byte for sequence int marker = inputStream.read(); @@ -986,6 +1084,7 @@ private void listenForEventPackets() throws IOException { } } } finally { + abortRequest = false; if (isConnected()) { if (completeShutdown) { disconnect(); // initiate complete shutdown sequence (which includes keep alive thread) @@ -1027,7 +1126,7 @@ private void updateClientBinlogFilenameAndPosition(Event event) { } } - private void updateGtidSet(Event event) { + protected void updateGtidSet(Event event) { synchronized (gtidSetAccessLock) { if (gtidSet == null) { return; @@ -1049,21 +1148,43 @@ private void updateGtidSet(Event event) { if (sql == null) { break; } - if ("BEGIN".equals(sql)) { - tx = true; - } else - if ("COMMIT".equals(sql) || "ROLLBACK".equals(sql)) { - commitGtid(); - tx = false; - } else - if (!tx) { - // auto-commit query, likely DDL - commitGtid(); + commitGtid(sql); + break; + case ANNOTATE_ROWS: + AnnotateRowsEventData annotateRowsEventData = (AnnotateRowsEventData) EventDeserializer.EventDataWrapper.internal(event.getData()); + sql = annotateRowsEventData.getRowsQuery(); + if (sql == null) { + break; } + commitGtid(sql); + break; + case MARIADB_GTID: + MariadbGtidEventData mariadbGtidEventData = (MariadbGtidEventData) EventDeserializer.EventDataWrapper.internal(event.getData()); + mariadbGtidEventData.setServerId(eventHeader.getServerId()); + gtid = mariadbGtidEventData.toString(); + break; + case MARIADB_GTID_LIST: + MariadbGtidListEventData mariadbGtidListEventData = (MariadbGtidListEventData) EventDeserializer.EventDataWrapper.internal(event.getData()); + gtid = mariadbGtidListEventData.getMariaGTIDSet().toString(); + break; default: } } + protected void commitGtid(String sql) { + if ("BEGIN".equals(sql)) { + tx = true; + } else + if ("COMMIT".equals(sql) || "ROLLBACK".equals(sql)) { + commitGtid(); + tx = false; + } else + if (!tx) { + // auto-commit query, likely DDL + commitGtid(); + } + } + private void commitGtid() { if (gtid != null) { synchronized (gtidSetAccessLock) { @@ -1072,17 +1193,14 @@ private void commitGtid() { } } - private ResultSetRowPacket[] readResultSet() throws IOException { - List resultSet = new LinkedList(); + protected ResultSetRowPacket[] readResultSet() throws IOException { + List resultSet = new LinkedList<>(); byte[] statementResult = channel.read(); - if (statementResult[0] == (byte) 0xFF /* error */) { - byte[] bytes = Arrays.copyOfRange(statementResult, 1, statementResult.length); - ErrorPacket errorPacket = new ErrorPacket(bytes); - throw new ServerException(errorPacket.getErrorMessage(), errorPacket.getErrorCode(), - errorPacket.getSqlState()); - } + checkError(statementResult); + while ((channel.read())[0] != (byte) 0xFE /* eof */) { /* skip */ } for (byte[] bytes; (bytes = channel.read())[0] != (byte) 0xFE /* eof */; ) { + checkError(bytes); resultSet.add(new ResultSetRowPacket(bytes)); } return resultSet.toArray(new ResultSetRowPacket[resultSet.size()]); @@ -1098,6 +1216,7 @@ public List getEventListeners() { /** * Register event listener. Note that multiple event listeners will be called in order they * where registered. + * @param eventListener event listener */ public void registerEventListener(EventListener eventListener) { eventListeners.add(eventListener); @@ -1105,6 +1224,7 @@ public void registerEventListener(EventListener eventListener) { /** * Unregister all event listener of specific type. + * @param listenerClass event listener class to unregister */ public void unregisterEventListener(Class listenerClass) { for (EventListener eventListener: eventListeners) { @@ -1116,6 +1236,7 @@ public void unregisterEventListener(Class listenerClass /** * Unregister single event listener. + * @param eventListener event listener to unregister */ public void unregisterEventListener(EventListener eventListener) { eventListeners.remove(eventListener); @@ -1129,9 +1250,8 @@ private void notifyEventListeners(Event event) { try { eventListener.onEvent(event); } catch (Exception e) { - if (logger.isLoggable(Level.WARNING)) { - logger.log(Level.WARNING, eventListener + " choked on " + event, e); - } + throw new RuntimeException("Binlog event listener " + eventListener + + " choked on " + event, e); } } } @@ -1146,6 +1266,7 @@ public List getLifecycleListeners() { /** * Register lifecycle listener. Note that multiple lifecycle listeners will be called in order they * where registered. + * @param lifecycleListener lifecycle listener to register */ public void registerLifecycleListener(LifecycleListener lifecycleListener) { lifecycleListeners.add(lifecycleListener); @@ -1153,6 +1274,7 @@ public void registerLifecycleListener(LifecycleListener lifecycleListener) { /** * Unregister all lifecycle listener of specific type. + * @param listenerClass lifecycle listener class to unregister */ public void unregisterLifecycleListener(Class listenerClass) { for (LifecycleListener lifecycleListener : lifecycleListeners) { @@ -1164,6 +1286,7 @@ public void unregisterLifecycleListener(Class liste /** * Unregister single lifecycle listener. + * @param eventListener lifecycle listener to unregister */ public void unregisterLifecycleListener(LifecycleListener eventListener) { lifecycleListeners.remove(eventListener); @@ -1179,21 +1302,25 @@ public void disconnect() throws IOException { terminateConnect(); } + public void abort() { + abortRequest = true; + } + private void terminateKeepAliveThread() { try { keepAliveThreadExecutorLock.lock(); ExecutorService keepAliveThreadExecutor = this.keepAliveThreadExecutor; - if (keepAliveThreadExecutor == null) { + if ( keepAliveThreadExecutor == null ) { return; } keepAliveThreadExecutor.shutdownNow(); - while (!awaitTerminationInterruptibly(keepAliveThreadExecutor, - Long.MAX_VALUE, TimeUnit.NANOSECONDS)) { - // ignore - } } finally { keepAliveThreadExecutorLock.unlock(); } + while (!awaitTerminationInterruptibly(keepAliveThreadExecutor, + Long.MAX_VALUE, TimeUnit.NANOSECONDS)) { + // ignore + } } private static boolean awaitTerminationInterruptibly(ExecutorService executorService, long timeout, TimeUnit unit) { @@ -1241,23 +1368,29 @@ public interface LifecycleListener { /** * Called once client has successfully logged in but before started to receive binlog events. + * @param client the client that logged in */ void onConnect(BinaryLogClient client); /** * It's guarantied to be called before {@link #onDisconnect(BinaryLogClient)}) in case of * communication failure. + * @param client the client that triggered the communication failure + * @param ex The exception that triggered the communication failutre */ void onCommunicationFailure(BinaryLogClient client, Exception ex); /** * Called in case of failed event deserialization. Note this type of error does NOT cause client to * disconnect. If you wish to stop receiving events you'll need to fire client.disconnect() manually. + * @param client the client that failed event deserialization + * @param ex The exception that triggered the failutre */ void onEventDeserializationFailure(BinaryLogClient client, Exception ex); /** * Called upon disconnect (regardless of the reason). + * @param client the client that disconnected */ void onDisconnect(BinaryLogClient client); } diff --git a/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogFileReader.java b/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogFileReader.java index 96b79fc6..d06a2d2d 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogFileReader.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/BinaryLogFileReader.java @@ -77,6 +77,7 @@ public BinaryLogFileReader(InputStream inputStream, EventDeserializer eventDeser /** * @return deserialized event or null in case of end-of-stream + * @throws IOException if reading the event fails */ public Event readEvent() throws IOException { return eventDeserializer.nextEvent(inputStream); diff --git a/src/main/java/com/github/shyiko/mysql/binlog/GtidSet.java b/src/main/java/com/github/shyiko/mysql/binlog/GtidSet.java index 6899eafe..fb68b54c 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/GtidSet.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/GtidSet.java @@ -31,7 +31,7 @@ * gtid_set: uuid_set[,uuid_set]... * uuid_set: uuid:interval[:interval]... * uuid: hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh, h: [0-9|A-F] - * interval: n[-n], (n >= 1) + * interval: n[-n], (n >= 1) * * * @author Stanley Shyiko @@ -40,6 +40,13 @@ public class GtidSet { private final Map map = new LinkedHashMap(); + public static GtidSet parse(String gtidStr) { + if ( MariadbGtidSet.isMariaGtidSet(gtidStr) ) { + return new MariadbGtidSet(gtidStr); + } else { + return new GtidSet(gtidStr); + } + } /** * @param gtidSet gtid set comprised of closed intervals (like MySQL's executed_gtid_set). */ @@ -160,6 +167,10 @@ public String toString() { return join(gtids, ","); } + public String toSeenString() { + return this.toString(); + } + private static String join(Collection o, String delimiter) { if (o.isEmpty()) { return ""; diff --git a/src/main/java/com/github/shyiko/mysql/binlog/MariadbGtidSet.java b/src/main/java/com/github/shyiko/mysql/binlog/MariadbGtidSet.java new file mode 100644 index 00000000..91c854e1 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/MariadbGtidSet.java @@ -0,0 +1,240 @@ +package com.github.shyiko.mysql.binlog; + +import java.util.*; +import java.util.regex.Pattern; + +/** + * Mariadb Global Transaction ID + * + * @author Winger + * @see GTID for the original doc + */ +public class MariadbGtidSet extends GtidSet { + /* + we keep two maps; one of them contains the current GTID position for + each domain. The other contains all the "seen" GTID positions for each + domain and can be used to compare against another gtid postion. + */ + protected Map positionMap = new HashMap<>(); + + protected Map> seenMap = new LinkedHashMap<>(); + + public MariadbGtidSet() { + super(null); // + } + + /** + * Initialize a new MariaDB gtid set from a string, like: + * 0-1-24,0-555555-9709 + * DOMAIN_ID-SERVER_ID-SEQUENCE[,DOMAIN_ID-SERVER_ID-SEQUENCE] + * + * note that for duplicate domain ids it's "last one wins" for the current position + * @param gtidSet a string representing the gtid set. + */ + public MariadbGtidSet(String gtidSet) { + super(null); + if (gtidSet != null && gtidSet.length() > 0) { + String[] gtids = gtidSet.replaceAll("\n", "").split(","); + for (String gtid : gtids) { + MariaGtid mariaGtid = MariaGtid.parse(gtid); + + positionMap.put(mariaGtid.getDomainId(), mariaGtid); + addToSeenSet(mariaGtid); + } + } + } + + static String threeDashes = "\\d{1,10}-\\d{1,10}-\\d{1,20}"; + + static Pattern MARIA_GTID_PATTERN = Pattern.compile( + "^" + threeDashes + "(\\s*,\\s*" + threeDashes + ")*$" + ); + + public static boolean isMariaGtidSet(String gtidSet) { + return MARIA_GTID_PATTERN.matcher(gtidSet).find(); + } + + private void addToSeenSet(MariaGtid gtid) { + if ( !this.seenMap.containsKey(gtid.domainId) ) { + this.seenMap.put(gtid.domainId, new LinkedHashMap<>()); + } + + LinkedHashMap domainMap = this.seenMap.get(gtid.domainId); + domainMap.put(gtid.serverId, gtid); + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (MariaGtid gtid : positionMap.values()) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(gtid.toString()); + } + return sb.toString(); + } + + @Override + public String toSeenString() { + StringBuilder sb = new StringBuilder(); + for (Long domainID : seenMap.keySet()) { + for( MariaGtid gtid: seenMap.get(domainID).values() ) { + if (sb.length() > 0) { + sb.append(","); + } + + sb.append(gtid.toString()); + } + } + return sb.toString(); + } + + @Override + public Collection getUUIDSets() { + throw new UnsupportedOperationException("Mariadb gtid not support this method"); + } + + @Override + public UUIDSet getUUIDSet(String uuid) { + throw new UnsupportedOperationException("Mariadb gtid not support this method"); + } + + @Override + public UUIDSet putUUIDSet(UUIDSet uuidSet) { + throw new UnsupportedOperationException("Mariadb gtid not support this method"); + } + + @Override + public boolean add(String gtid) { + MariaGtid mariaGtid = MariaGtid.parse(gtid); + add(mariaGtid); + return true; + } + + public void add(MariaGtid gtid) { + positionMap.put(gtid.getDomainId(), gtid); + addToSeenSet(gtid); + } + + /* + we're trying to ask "is this position behind the other position?" + - if we have a domain that the other doesn't, we're probably "ahead". + - the inverse is true too + */ + @Override + public boolean isContainedWithin(GtidSet other) { + if (!(other instanceof MariadbGtidSet)) + return false; + + MariadbGtidSet o = (MariadbGtidSet) other; + + for ( Long domainID : this.seenMap.keySet() ) { + if ( !o.seenMap.containsKey(domainID) ) { + return false; + } + + LinkedHashMap thisDomainMap = this.seenMap.get(domainID); + LinkedHashMap otherDomainMap = o.seenMap.get(domainID); + + for ( Long serverID : thisDomainMap.keySet() ) { + if ( !otherDomainMap.containsKey(serverID)) { + return false; + } + + MariaGtid thisGtid = thisDomainMap.get(serverID); + MariaGtid otherGtid = otherDomainMap.get(serverID); + if ( thisGtid.sequence > otherGtid.sequence ) + return false; + } + } + return true; + } + + @Override + public int hashCode() { + return this.seenMap.keySet().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof MariadbGtidSet) { + MariadbGtidSet that = (MariadbGtidSet) obj; + return this.positionMap.equals(that.positionMap); + } + return false; + } + + public static class MariaGtid { + + // {domainId}-{serverId}-{sequence} + private long domainId; + private long serverId; + private long sequence; + + public MariaGtid(long domainId, long serverId, long sequence) { + this.domainId = domainId; + this.serverId = serverId; + this.sequence = sequence; + } + + public MariaGtid(String gtid) { + String[] gtidArr = gtid.split("-"); + this.domainId = Long.parseLong(gtidArr[0]); + this.serverId = Long.parseLong(gtidArr[1]); + this.sequence = Long.parseLong(gtidArr[2]); + } + + public static MariaGtid parse(String gtid) { + return new MariaGtid(gtid); + } + + public long getDomainId() { + return domainId; + } + + public void setDomainId(long domainId) { + this.domainId = domainId; + } + + public long getServerId() { + return serverId; + } + + public void setServerId(long serverId) { + this.serverId = serverId; + } + + public long getSequence() { + return sequence; + } + + public void setSequence(long sequence) { + this.sequence = sequence; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MariaGtid mariaGtid = (MariaGtid) o; + return domainId == mariaGtid.domainId && + serverId == mariaGtid.serverId && + sequence == mariaGtid.sequence; + } + + @Override + public String toString() { + return String.format("%s-%s-%s", domainId, serverId, sequence); + } + } +} + diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/AnnotateRowsEventData.java b/src/main/java/com/github/shyiko/mysql/binlog/event/AnnotateRowsEventData.java new file mode 100644 index 00000000..85fa79e0 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/AnnotateRowsEventData.java @@ -0,0 +1,31 @@ +package com.github.shyiko.mysql.binlog.event; + +/** + * Mariadb ANNOTATE_ROWS_EVENT events accompany row events and describe the query which caused the row event + * Enable this with --binlog-annotate-row-events (default on from MariaDB 10.2.4). + * In the binary log, each Annotate_rows event precedes the corresponding Table map event. + * Note the master server sends ANNOTATE_ROWS_EVENT events only if the Slave server connects + * with the BINLOG_SEND_ANNOTATE_ROWS_EVENT flag (value is 2) in the COM_BINLOG_DUMP Slave Registration phase. + * + * @author Winger + * @see ANNOTATE_ROWS_EVENT for the original doc + */ +public class AnnotateRowsEventData implements EventData { + + private String rowsQuery; + + public String getRowsQuery() { + return rowsQuery; + } + + public void setRowsQuery(String rowsQuery) { + this.rowsQuery = rowsQuery; + } + + @Override + public String toString() { + return "AnnotateRowsEventData{" + + "rowsQuery='" + rowsQuery + '\'' + + '}'; + } +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/BinlogCheckpointEventData.java b/src/main/java/com/github/shyiko/mysql/binlog/event/BinlogCheckpointEventData.java new file mode 100644 index 00000000..bd1fceed --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/BinlogCheckpointEventData.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 Stanley Shyiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.shyiko.mysql.binlog.event; + +/** + * @author Stanley Shyiko + */ + +public class BinlogCheckpointEventData implements EventData { + + private String logFileName; + + public void setLogFileName(String logFileName) { + this.logFileName = logFileName; + } + + public String getLogFileName() { + return this.logFileName; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("BinlogCheckpointEventData"); + sb.append("{logFileName=").append(logFileName); + sb.append('}'); + return sb.toString(); + } + +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/EventType.java b/src/main/java/com/github/shyiko/mysql/binlog/event/EventType.java index 7f94ccfd..e6e9564a 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/event/EventType.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/EventType.java @@ -16,99 +16,99 @@ package com.github.shyiko.mysql.binlog.event; /** + * @author Stanley Shyiko * @see Event Meanings for the original * documentation. - * @author Stanley Shyiko */ public enum EventType { /** * Events of this event type should never occur. Not written to a binary log. */ - UNKNOWN, + UNKNOWN(0), /** * A descriptor event that is written to the beginning of the each binary log file. (In MySQL 4.0 and 4.1, * this event is written only to the first binary log file that the server creates after startup.) This event is * used in MySQL 3.23 through 4.1 and superseded in MySQL 5.0 by {@link #FORMAT_DESCRIPTION}. */ - START_V3, + START_V3(1), /** * Written when an updating statement is done. */ - QUERY, + QUERY(2), /** * Written when mysqld stops. */ - STOP, + STOP(3), /** * Written when mysqld switches to a new binary log file. This occurs when someone issues a FLUSH LOGS statement or * the current binary log file becomes larger than max_binlog_size. */ - ROTATE, + ROTATE(4), /** * Written every time a statement uses an AUTO_INCREMENT column or the LAST_INSERT_ID() function; precedes other * events for the statement. This is written only before a {@link #QUERY} and is not used in case of RBR. */ - INTVAR, + INTVAR(5), /** * Used for LOAD DATA INFILE statements in MySQL 3.23. */ - LOAD, + LOAD(6), /** * Not used. */ - SLAVE, + SLAVE(7), /** * Used for LOAD DATA INFILE statements in MySQL 4.0 and 4.1. */ - CREATE_FILE, + CREATE_FILE(8), /** * Used for LOAD DATA INFILE statements as of MySQL 4.0. */ - APPEND_BLOCK, + APPEND_BLOCK(9), /** * Used for LOAD DATA INFILE statements in 4.0 and 4.1. */ - EXEC_LOAD, + EXEC_LOAD(10), /** * Used for LOAD DATA INFILE statements as of MySQL 4.0. */ - DELETE_FILE, + DELETE_FILE(11), /** * Used for LOAD DATA INFILE statements in MySQL 4.0 and 4.1. */ - NEW_LOAD, + NEW_LOAD(12), /** * Written every time a statement uses the RAND() function; precedes other events for the statement. Indicates the * seed values to use for generating a random number with RAND() in the next statement. This is written only * before a {@link #QUERY} and is not used in case of RBR. */ - RAND, + RAND(13), /** * Written every time a statement uses a user variable; precedes other events for the statement. Indicates the * value to use for the user variable in the next statement. This is written only before a {@link #QUERY} and * is not used in case of RBR. */ - USER_VAR, + USER_VAR(14), /** * A descriptor event that is written to the beginning of the each binary log file. * This event is used as of MySQL 5.0; it supersedes {@link #START_V3}. */ - FORMAT_DESCRIPTION, + FORMAT_DESCRIPTION(15), /** * Generated for a commit of a transaction that modifies one or more tables of an XA-capable storage engine. * Normal transactions are implemented by sending a {@link #QUERY} containing a BEGIN statement and a {@link #QUERY} * containing a COMMIT statement (or a ROLLBACK statement if the transaction is rolled back). */ - XID, + XID(16), /** * Used for LOAD DATA INFILE statements as of MySQL 5.0. */ - BEGIN_LOAD_QUERY, + BEGIN_LOAD_QUERY(17), /** * Used for LOAD DATA INFILE statements as of MySQL 5.0. */ - EXECUTE_LOAD_QUERY, + EXECUTE_LOAD_QUERY(18), /** * This event precedes each row operation event. It maps a table definition to a number, where the table definition * consists of database and table names and column definitions. The purpose of this event is to enable replication @@ -117,105 +117,152 @@ public enum EventType { * of TABLE_MAP events: one per table used by events in the sequence. * Used in case of RBR. */ - TABLE_MAP, + TABLE_MAP(19), /** * Describes inserted rows (within a single table). * Used in case of RBR (5.1.0 - 5.1.15). */ - PRE_GA_WRITE_ROWS, + PRE_GA_WRITE_ROWS(20), /** * Describes updated rows (within a single table). * Used in case of RBR (5.1.0 - 5.1.15). */ - PRE_GA_UPDATE_ROWS, + PRE_GA_UPDATE_ROWS(21), /** * Describes deleted rows (within a single table). * Used in case of RBR (5.1.0 - 5.1.15). */ - PRE_GA_DELETE_ROWS, + PRE_GA_DELETE_ROWS(22), /** * Describes inserted rows (within a single table). * Used in case of RBR (5.1.16 - mysql-trunk). */ - WRITE_ROWS, + WRITE_ROWS(23), /** * Describes updated rows (within a single table). * Used in case of RBR (5.1.16 - mysql-trunk). */ - UPDATE_ROWS, + UPDATE_ROWS(24), /** * Describes deleted rows (within a single table). * Used in case of RBR (5.1.16 - mysql-trunk). */ - DELETE_ROWS, + DELETE_ROWS(25), /** * Used to log an out of the ordinary event that occurred on the master. It notifies the slave that something * happened on the master that might cause data to be in an inconsistent state. */ - INCIDENT, + INCIDENT(26), /** * Sent by a master to a slave to let the slave know that the master is still alive. Not written to a binary log. */ - HEARTBEAT, + HEARTBEAT(27), /** * In some situations, it is necessary to send over ignorable data to the slave: data that a slave can handle in * case there is code for handling it, but which can be ignored if it is not recognized. */ - IGNORABLE, + IGNORABLE(28), /** * Introduced to record the original query for rows events in RBR. */ - ROWS_QUERY, + ROWS_QUERY(29), /** * Describes inserted rows (within a single table). * Used in case of RBR (5.1.18+). */ - EXT_WRITE_ROWS, + EXT_WRITE_ROWS(30), /** * Describes updated rows (within a single table). * Used in case of RBR (5.1.18+). */ - EXT_UPDATE_ROWS, + EXT_UPDATE_ROWS(31), /** * Describes deleted rows (within a single table). * Used in case of RBR (5.1.18+). */ - EXT_DELETE_ROWS, + EXT_DELETE_ROWS(32), /** * Global Transaction Identifier. */ - GTID, - ANONYMOUS_GTID, - PREVIOUS_GTIDS, - TRANSACTION_CONTEXT, - VIEW_CHANGE, + GTID(33), + ANONYMOUS_GTID(34), + PREVIOUS_GTIDS(35), + TRANSACTION_CONTEXT(36), + VIEW_CHANGE(37), /** * Prepared XA transaction terminal event similar to XID except that it is specific to XA transaction. */ - XA_PREPARE; + XA_PREPARE(38), + /** + Extension of UPDATE_ROWS_EVENT, allowing partial values according + to binlog_row_value_options. + */ + PARTIAL_UPDATE_ROWS_EVENT(39), + /** + * Generated when 'binlog_transaction_compression' is set to 'ON'. + * It encapsulates all the events of a transaction in a Zstd compressed payload. + */ + TRANSACTION_PAYLOAD(40), + + /** + * MariaDB Support Events + * + * @see Replication Protocol for the original doc. + */ + ANNOTATE_ROWS(160), // + BINLOG_CHECKPOINT(161), + MARIADB_GTID(162), + MARIADB_GTID_LIST(163); + + private final int eventId; + + EventType(int eventId) { + this.eventId = eventId; + } + + /** + * Parses the event type based on the ordinal. + * + *

If an invalid or proprietary ordinal is passed, EventType.UNKNOWN is returned. + */ + public static EventType forId(int eventId) { + for (EventType type : EventType.values()) { + if (type.eventId == eventId) return type; + } + + return EventType.UNKNOWN; + } public static boolean isRowMutation(EventType eventType) { return EventType.isWrite(eventType) || - EventType.isUpdate(eventType) || - EventType.isDelete(eventType); + EventType.isUpdate(eventType) || + EventType.isDelete(eventType); } public static boolean isWrite(EventType eventType) { return eventType == PRE_GA_WRITE_ROWS || - eventType == WRITE_ROWS || - eventType == EXT_WRITE_ROWS; + eventType == WRITE_ROWS || + eventType == EXT_WRITE_ROWS; } public static boolean isUpdate(EventType eventType) { return eventType == PRE_GA_UPDATE_ROWS || - eventType == UPDATE_ROWS || - eventType == EXT_UPDATE_ROWS; + eventType == UPDATE_ROWS || + eventType == EXT_UPDATE_ROWS; } public static boolean isDelete(EventType eventType) { return eventType == PRE_GA_DELETE_ROWS || - eventType == DELETE_ROWS || - eventType == EXT_DELETE_ROWS; + eventType == DELETE_ROWS || + eventType == EXT_DELETE_ROWS; } + public static EventType byEventNumber(int num) { + for (EventType type : EventType.values()) { + if (type.eventId == num) { + return type; + } + } + return null; + } } diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/LRUCache.java b/src/main/java/com/github/shyiko/mysql/binlog/event/LRUCache.java new file mode 100644 index 00000000..784cec0e --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/LRUCache.java @@ -0,0 +1,19 @@ +package com.github.shyiko.mysql.binlog.event; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class LRUCache extends LinkedHashMap { + private int maxSize; + + // and other constructors for load factor and hashtable capacity + public LRUCache(int initialCapacity, float loadFactor, int maxSize) { + super(initialCapacity, loadFactor, true); + this.maxSize = maxSize; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > maxSize; + } +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/MariadbGtidEventData.java b/src/main/java/com/github/shyiko/mysql/binlog/event/MariadbGtidEventData.java new file mode 100644 index 00000000..43f0c55c --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/MariadbGtidEventData.java @@ -0,0 +1,59 @@ +package com.github.shyiko.mysql.binlog.event; + +/** + * MariaDB and MySQL have different GTID implementations, and that these are not compatible with each other. + * + * @author Winger + * @see GTID_EVENT for the original doc + */ +public class MariadbGtidEventData implements EventData { + public static int FL_STANDALONE = 1; + public static int FL_GROUP_COMMIT_ID = 2; + public static int FL_TRANSACTIONAL = 4; + public static int FL_ALLOW_PARALLEL = 8; + public static int FL_WAITED = 16; + public static int FL_DDL = 32; + + private long sequence; + private long domainId; + private long serverId; + + private int flags; + + public long getSequence() { + return sequence; + } + + public void setSequence(long sequence) { + this.sequence = sequence; + } + + public long getDomainId() { + return domainId; + } + + public void setDomainId(long domainId) { + this.domainId = domainId; + } + + public long getServerId() { + return serverId; + } + + public void setServerId(long serverId) { + this.serverId = serverId; + } + + public int getFlags() { + return flags; + } + + public void setFlags(int flags) { + this.flags = flags; + } + + @Override + public String toString() { + return domainId + "-" + serverId + "-" + sequence; + } +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/MariadbGtidListEventData.java b/src/main/java/com/github/shyiko/mysql/binlog/event/MariadbGtidListEventData.java new file mode 100644 index 00000000..ae6e91f6 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/MariadbGtidListEventData.java @@ -0,0 +1,29 @@ +package com.github.shyiko.mysql.binlog.event; + +import com.github.shyiko.mysql.binlog.MariadbGtidSet; + +/** + * Logged in every binlog to record the current replication state + * + * @author Winger + * @see GTID_LIST_EVENT for the original doc + */ +public class MariadbGtidListEventData implements EventData { + + private MariadbGtidSet mariaGTIDSet; + + public MariadbGtidSet getMariaGTIDSet() { + return mariaGTIDSet; + } + + public void setMariaGTIDSet(MariadbGtidSet mariaGTIDSet) { + this.mariaGTIDSet = mariaGTIDSet; + } + + @Override + public String toString() { + return "MariadbGtidListEventData{" + + "mariaGTIDSet=" + mariaGTIDSet + + '}'; + } +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/TableMapEventMetadata.java b/src/main/java/com/github/shyiko/mysql/binlog/event/TableMapEventMetadata.java index 66f030ec..8b4a48ec 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/event/TableMapEventMetadata.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/TableMapEventMetadata.java @@ -36,6 +36,7 @@ public class TableMapEventMetadata implements EventData { private Map primaryKeysWithPrefix; private DefaultCharset enumAndSetDefaultCharset; private List enumAndSetColumnCharsets; + private BitSet visibility; public BitSet getSignedness() { return signedness; @@ -125,6 +126,14 @@ public void setEnumAndSetColumnCharsets(List enumAndSetColumnCharsets) this.enumAndSetColumnCharsets = enumAndSetColumnCharsets; } + public BitSet getVisibility() { + return visibility; + } + + public void setVisibility(BitSet visibility) { + this.visibility = visibility; + } + @Override public String toString() { final StringBuilder sb = new StringBuilder(); @@ -163,6 +172,8 @@ public String toString() { sb.append(", enumAndSetColumnCharsets=").append(enumAndSetColumnCharsets == null ? "null" : ""); appendList(sb, enumAndSetColumnCharsets); + sb.append(",visibility=").append(visibility); + sb.append('}'); return sb.toString(); } diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/TransactionPayloadEventData.java b/src/main/java/com/github/shyiko/mysql/binlog/event/TransactionPayloadEventData.java new file mode 100644 index 00000000..f5014cc8 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/TransactionPayloadEventData.java @@ -0,0 +1,67 @@ +package com.github.shyiko.mysql.binlog.event; + +import java.util.ArrayList; + + +public class TransactionPayloadEventData implements EventData { + private int payloadSize; + private int uncompressedSize; + private int compressionType; + private byte[] payload; + private ArrayList uncompressedEvents; + + public ArrayList getUncompressedEvents() { + return uncompressedEvents; + } + + public void setUncompressedEvents(ArrayList uncompressedEvents) { + this.uncompressedEvents = uncompressedEvents; + } + + public int getPayloadSize() { + return payloadSize; + } + + public void setPayloadSize(int payloadSize) { + this.payloadSize = payloadSize; + } + + public int getUncompressedSize() { + return uncompressedSize; + } + + public void setUncompressedSize(int uncompressedSize) { + this.uncompressedSize = uncompressedSize; + } + + public int getCompressionType() { + return compressionType; + } + + public void setCompressionType(int compressionType) { + this.compressionType = compressionType; + } + + public byte[] getPayload() { + return payload; + } + + public void setPayload(byte[] payload) { + this.payload = payload; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("TransactionPayloadEventData"); + sb.append("{compression_type=").append(compressionType).append(", payload_size=").append(payloadSize).append(", uncompressed_size='").append(uncompressedSize).append('\''); + sb.append(", payload: "); + sb.append("\n"); + for (Event e : uncompressedEvents) { + sb.append(e.toString()); + sb.append("\n"); + } + sb.append("}"); + return sb.toString(); + } +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/AbstractRowsEventDataDeserializer.java b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/AbstractRowsEventDataDeserializer.java index e7cf6d53..3504a894 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/AbstractRowsEventDataDeserializer.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/AbstractRowsEventDataDeserializer.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.Serializable; import java.math.BigDecimal; +import java.sql.Time; import java.util.BitSet; import java.util.Calendar; import java.util.Map; @@ -75,6 +76,7 @@ public abstract class AbstractRowsEventDataDeserializer imp private Long invalidDateAndTimeRepresentation; private boolean microsecondsPrecision; private boolean deserializeCharAndBinaryAsByteArray; + private boolean deserializeIntegerAsByteArray; public AbstractRowsEventDataDeserializer(Map tableMapEventByTableId) { this.tableMapEventByTableId = tableMapEventByTableId; @@ -97,6 +99,10 @@ void setDeserializeCharAndBinaryAsByteArray(boolean value) { this.deserializeCharAndBinaryAsByteArray = value; } + void setDeserializeIntegerAsByteArray(boolean deserializeIntegerAsByteArray) { + this.deserializeIntegerAsByteArray = deserializeIntegerAsByteArray; + } + protected Serializable[] deserializeRow(long tableId, BitSet includedColumns, ByteArrayInputStream inputStream) throws IOException { TableMapEventData tableMapEvent = tableMapEventByTableId.get(tableId); @@ -203,22 +209,37 @@ protected Serializable deserializeBit(int meta, ByteArrayInputStream inputStream } protected Serializable deserializeTiny(ByteArrayInputStream inputStream) throws IOException { + if (deserializeIntegerAsByteArray) { + return inputStream.read(1); + } return (int) ((byte) inputStream.readInteger(1)); } protected Serializable deserializeShort(ByteArrayInputStream inputStream) throws IOException { + if (deserializeIntegerAsByteArray) { + return inputStream.read(2); + } return (int) ((short) inputStream.readInteger(2)); } protected Serializable deserializeInt24(ByteArrayInputStream inputStream) throws IOException { + if (deserializeIntegerAsByteArray) { + return inputStream.read(3); + } return (inputStream.readInteger(3) << 8) >> 8; } protected Serializable deserializeLong(ByteArrayInputStream inputStream) throws IOException { + if (deserializeIntegerAsByteArray) { + return inputStream.read(4); + } return inputStream.readInteger(4); } protected Serializable deserializeLongLong(ByteArrayInputStream inputStream) throws IOException { + if (deserializeIntegerAsByteArray) { + return inputStream.read(8); + } return inputStream.readLong(8); } @@ -265,7 +286,7 @@ protected Serializable deserializeTime(ByteArrayInputStream inputStream) throws if (deserializeDateAndTimeAsLong) { return castTimestamp(timestamp, 0); } - return timestamp != null ? new java.sql.Time(timestamp) : null; + return timestamp != null ? new java.sql.Timestamp(timestamp) : null; } protected Serializable deserializeTimeV2(int meta, ByteArrayInputStream inputStream) throws IOException { @@ -293,7 +314,7 @@ protected Serializable deserializeTimeV2(int meta, ByteArrayInputStream inputStr if (deserializeDateAndTimeAsLong) { return castTimestamp(timestamp, fsp); } - return timestamp != null ? new java.sql.Time(timestamp) : null; + return timestamp != null ? convertLongTimestamptWithFSP(timestamp, fsp) : null; } protected Serializable deserializeTimestamp(ByteArrayInputStream inputStream) throws IOException { @@ -311,7 +332,7 @@ protected Serializable deserializeTimestampV2(int meta, ByteArrayInputStream inp if (deserializeDateAndTimeAsLong) { return castTimestamp(timestamp, fsp); } - return new java.sql.Timestamp(timestamp); + return convertLongTimestamptWithFSP(timestamp, fsp); } protected Serializable deserializeDatetime(ByteArrayInputStream inputStream) throws IOException { @@ -320,7 +341,7 @@ protected Serializable deserializeDatetime(ByteArrayInputStream inputStream) thr if (deserializeDateAndTimeAsLong) { return castTimestamp(timestamp, 0); } - return timestamp != null ? new java.util.Date(timestamp) : null; + return timestamp != null ? new java.sql.Timestamp(timestamp) : null; } protected Serializable deserializeDatetimeV2(int meta, ByteArrayInputStream inputStream) throws IOException { @@ -353,11 +374,19 @@ protected Serializable deserializeDatetimeV2(int meta, ByteArrayInputStream inpu if (deserializeDateAndTimeAsLong) { return castTimestamp(timestamp, fsp); } - return timestamp != null ? new java.util.Date(timestamp) : null; + + return timestamp != null ? convertLongTimestamptWithFSP(timestamp, fsp) : null; + } + + private java.sql.Timestamp convertLongTimestamptWithFSP(Long timestamp, int fsp) { + java.sql.Timestamp ts = new java.sql.Timestamp(timestamp); + ts.setNanos(fsp * 1000); + return ts; } protected Serializable deserializeYear(ByteArrayInputStream inputStream) throws IOException { - return 1900 + inputStream.readInteger(1); + int year = inputStream.readInteger(1); + return year == 0 ? 0 : 1900 + year; } protected Serializable deserializeString(int length, ByteArrayInputStream inputStream) throws IOException { @@ -411,7 +440,6 @@ protected byte[] deserializeJson(int meta, ByteArrayInputStream inputStream) thr return inputStream.read(blobLength); } - // checkstyle, please ignore ParameterNumber for the next line protected Long asUnixTime(int year, int month, int day, int hour, int minute, int second, int millis) { // https://dev.mysql.com/doc/refman/5.0/en/datetime.html if (year == 0 || month == 0 || day == 0) { @@ -452,9 +480,6 @@ private static int[] split(long value, int divider, int length) { return result; } - /** - * see mysql/strings/decimal.c - */ public static BigDecimal asBigDecimal(int precision, int scale, byte[] value) { boolean positive = (value[0] & 0x80) == 0x80; value[0] ^= 0x80; diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/AnnotateRowsEventDataDeserializer.java b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/AnnotateRowsEventDataDeserializer.java new file mode 100644 index 00000000..981d9c94 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/AnnotateRowsEventDataDeserializer.java @@ -0,0 +1,24 @@ +package com.github.shyiko.mysql.binlog.event.deserialization; + +import com.github.shyiko.mysql.binlog.event.AnnotateRowsEventData; +import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; + +import java.io.IOException; + +/** + * Mariadb ANNOTATE_ROWS_EVENT Fields + *

+ *  string<EOF> The SQL statement (not null-terminated)
+ * 
+ * + * @author Winger + */ +public class AnnotateRowsEventDataDeserializer implements EventDataDeserializer { + + @Override + public AnnotateRowsEventData deserialize(ByteArrayInputStream inputStream) throws IOException { + AnnotateRowsEventData event = new AnnotateRowsEventData(); + event.setRowsQuery(inputStream.readString(inputStream.available())); + return event; + } +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/BinlogCheckpointEventDataDeserializer.java b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/BinlogCheckpointEventDataDeserializer.java new file mode 100644 index 00000000..ae456b4d --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/BinlogCheckpointEventDataDeserializer.java @@ -0,0 +1,36 @@ + +/* + * Copyright 2013 Stanley Shyiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.shyiko.mysql.binlog.event.deserialization; + +import com.github.shyiko.mysql.binlog.event.BinlogCheckpointEventData; +import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; + +import java.io.IOException; + +/** + * @author Stanley Shyiko + */ +public class BinlogCheckpointEventDataDeserializer implements EventDataDeserializer { + + @Override + public BinlogCheckpointEventData deserialize(ByteArrayInputStream inputStream) throws IOException { + BinlogCheckpointEventData eventData = new BinlogCheckpointEventData(); + int length = inputStream.readInteger(4); + eventData.setLogFileName(inputStream.readString(length)); + return eventData; + } +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventDeserializer.java b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventDeserializer.java index 5ca3e692..81dea256 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventDeserializer.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventDeserializer.java @@ -20,7 +20,9 @@ import com.github.shyiko.mysql.binlog.event.EventHeader; import com.github.shyiko.mysql.binlog.event.EventType; import com.github.shyiko.mysql.binlog.event.FormatDescriptionEventData; +import com.github.shyiko.mysql.binlog.event.LRUCache; import com.github.shyiko.mysql.binlog.event.TableMapEventData; +import com.github.shyiko.mysql.binlog.event.TransactionPayloadEventData; import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; import java.io.IOException; @@ -65,7 +67,7 @@ public EventDeserializer( this.eventHeaderDeserializer = eventHeaderDeserializer; this.defaultEventDataDeserializer = defaultEventDataDeserializer; this.eventDataDeserializers = new IdentityHashMap(); - this.tableMapEventByTableId = new HashMap(); + this.tableMapEventByTableId = new LRUCache<>(100, 0.75f, 10000); registerDefaultEventDataDeserializers(); afterEventDataDeserializerSet(null); } @@ -119,6 +121,16 @@ private void registerDefaultEventDataDeserializers() { new PreviousGtidSetDeserializer()); eventDataDeserializers.put(EventType.XA_PREPARE, new XAPrepareEventDataDeserializer()); + eventDataDeserializers.put(EventType.ANNOTATE_ROWS, + new AnnotateRowsEventDataDeserializer()); + eventDataDeserializers.put(EventType.MARIADB_GTID, + new MariadbGtidEventDataDeserializer()); + eventDataDeserializers.put(EventType.BINLOG_CHECKPOINT, + new BinlogCheckpointEventDataDeserializer()); + eventDataDeserializers.put(EventType.MARIADB_GTID_LIST, + new MariadbGtidListEventDataDeserializer()); + eventDataDeserializers.put(EventType.TRANSACTION_PAYLOAD, + new TransactionPayloadEventDataDeserializer()); } public void setEventDataDeserializer(EventType eventType, EventDataDeserializer eventDataDeserializer) { @@ -152,6 +164,7 @@ private void afterEventDataDeserializerSet(EventType eventType) { /** * @deprecated resolved based on FORMAT_DESCRIPTION + * @param checksumType don't use this function. */ @Deprecated public void setChecksumType(ChecksumType checksumType) { @@ -160,6 +173,8 @@ public void setChecksumType(ChecksumType checksumType) { /** * @see CompatibilityMode + * @param first at least one CompatabilityMode + * @param rest many modes */ public void setCompatibilityMode(CompatibilityMode first, CompatibilityMode... rest) { this.compatibilitySet = EnumSet.of(first, rest); @@ -199,11 +214,16 @@ private void ensureCompatibility(EventDataDeserializer eventDataDeserializer) { deserializer.setDeserializeCharAndBinaryAsByteArray( compatibilitySet.contains(CompatibilityMode.CHAR_AND_BINARY_AS_BYTE_ARRAY) ); + deserializer.setDeserializeIntegerAsByteArray( + compatibilitySet.contains(CompatibilityMode.INTEGER_AS_BYTE_ARRAY) + ); } } /** * @return deserialized event or null in case of end-of-stream + * @param inputStream input stream to fetch event from + * @throws IOException if connection gets closed */ public Event nextEvent(ByteArrayInputStream inputStream) throws IOException { if (inputStream.peek() == -1) { @@ -218,6 +238,9 @@ public Event nextEvent(ByteArrayInputStream inputStream) throws IOException { case TABLE_MAP: eventData = deserializeTableMapEventData(inputStream, eventHeader); break; + case TRANSACTION_PAYLOAD: + eventData = deserializeTransactionPayloadEventData(inputStream, eventHeader); + break; default: EventDataDeserializer eventDataDeserializer = getEventDataDeserializer(eventHeader.getEventType()); eventData = deserializeEventData(inputStream, eventHeader, eventDataDeserializer); @@ -263,6 +286,26 @@ private EventData deserializeFormatDescriptionEventData(ByteArrayInputStream inp return eventData; } + public EventData deserializeTransactionPayloadEventData(ByteArrayInputStream inputStream, EventHeader eventHeader) + throws IOException { + EventDataDeserializer eventDataDeserializer = eventDataDeserializers.get(EventType.TRANSACTION_PAYLOAD); + EventData eventData = deserializeEventData(inputStream, eventHeader, eventDataDeserializer); + TransactionPayloadEventData transactionPayloadEventData = (TransactionPayloadEventData) eventData; + + /** + * Handling for TABLE_MAP events withing the transaction payload event. This is to ensure that for the table map + * events within the transaction payload, the target table id and the event gets added to the + * tableMapEventByTableId map. This is map is later used while deserializing rows. + */ + for (Event event : transactionPayloadEventData.getUncompressedEvents()) { + if (event.getHeader().getEventType() == EventType.TABLE_MAP && event.getData() != null) { + TableMapEventData tableMapEvent = (TableMapEventData) event.getData(); + tableMapEventByTableId.put(tableMapEvent.getTableId(), tableMapEvent); + } + } + return eventData; + } + public EventData deserializeTableMapEventData(ByteArrayInputStream inputStream, EventHeader eventHeader) throws IOException { EventDataDeserializer eventDataDeserializer = @@ -349,7 +392,11 @@ public enum CompatibilityMode { * *

This option is going to be enabled by default starting from mysql-binlog-connector-java@1.0.0. */ - CHAR_AND_BINARY_AS_BYTE_ARRAY + CHAR_AND_BINARY_AS_BYTE_ARRAY, + /** + * Return TINY/SHORT/INT24/LONG/LONGLONG values as byte[]|s (instead of int|s). + */ + INTEGER_AS_BYTE_ARRAY } /** diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventHeaderV4Deserializer.java b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventHeaderV4Deserializer.java index 2d8bcdd8..27fa617b 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventHeaderV4Deserializer.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/EventHeaderV4Deserializer.java @@ -26,25 +26,15 @@ */ public class EventHeaderV4Deserializer implements EventHeaderDeserializer { - private static final EventType[] EVENT_TYPES = EventType.values(); - @Override public EventHeaderV4 deserialize(ByteArrayInputStream inputStream) throws IOException { EventHeaderV4 header = new EventHeaderV4(); header.setTimestamp(inputStream.readLong(4) * 1000L); - header.setEventType(getEventType(inputStream.readInteger(1))); + header.setEventType(EventType.forId(inputStream.readInteger(1))); header.setServerId(inputStream.readLong(4)); header.setEventLength(inputStream.readLong(4)); header.setNextPosition(inputStream.readLong(4)); header.setFlags(inputStream.readInteger(2)); return header; } - - private static EventType getEventType(int ordinal) throws IOException { - if (ordinal >= EVENT_TYPES.length) { - throw new IOException("Unknown event type " + ordinal); - } - return EVENT_TYPES[ordinal]; - } - } diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/MariadbGtidEventDataDeserializer.java b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/MariadbGtidEventDataDeserializer.java new file mode 100644 index 00000000..4f619b54 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/MariadbGtidEventDataDeserializer.java @@ -0,0 +1,34 @@ +package com.github.shyiko.mysql.binlog.event.deserialization; + +import com.github.shyiko.mysql.binlog.event.MariadbGtidEventData; +import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; + +import java.io.IOException; + +/** + * Mariadb GTID_EVENT Fields + *

+ *     uint8 GTID sequence
+ *     uint4 Replication Domain ID
+ *     uint1 Flags
+ *
+ * 	if flag & FL_GROUP_COMMIT_ID
+ * 	    uint8 commit_id
+ * 	else
+ * 	    uint6 0
+ * 
+ * + * @author Winger + * @see GTID_EVENT for the original doc + */ +public class MariadbGtidEventDataDeserializer implements EventDataDeserializer { + @Override + public MariadbGtidEventData deserialize(ByteArrayInputStream inputStream) throws IOException { + MariadbGtidEventData event = new MariadbGtidEventData(); + event.setSequence(inputStream.readLong(8)); + event.setDomainId(inputStream.readInteger(4)); + event.setFlags(inputStream.readInteger(1)); + // Flags ignore + return event; + } +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/MariadbGtidListEventDataDeserializer.java b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/MariadbGtidListEventDataDeserializer.java new file mode 100644 index 00000000..25836195 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/MariadbGtidListEventDataDeserializer.java @@ -0,0 +1,39 @@ +package com.github.shyiko.mysql.binlog.event.deserialization; + + +import com.github.shyiko.mysql.binlog.MariadbGtidSet; +import com.github.shyiko.mysql.binlog.event.MariadbGtidListEventData; +import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; + +import java.io.IOException; + +/** + * Mariadb GTID_LIST_EVENT Fields + *
+ *  uint4 Number of GTIDs
+ *  GTID[0]
+ *      uint4 Replication Domain ID
+ *      uint4 Server_ID
+ *      uint8 GTID sequence ...
+ * GTID[n]
+ * 
+ * + * @author Winger + * @see GTID_EVENT for the original doc + */ +public class MariadbGtidListEventDataDeserializer implements EventDataDeserializer { + @Override + public MariadbGtidListEventData deserialize(ByteArrayInputStream inputStream) throws IOException { + MariadbGtidListEventData eventData = new MariadbGtidListEventData(); + long gtidLength = inputStream.readInteger(4); + MariadbGtidSet mariaGTIDSet = new MariadbGtidSet(); + for (int i = 0; i < gtidLength; i++) { + long domainId = inputStream.readInteger(4); + long serverId = inputStream.readInteger(4); + long sequence = inputStream.readLong(8); + mariaGTIDSet.add(new MariadbGtidSet.MariaGtid(domainId, serverId, sequence)); + } + eventData.setMariaGTIDSet(mariaGTIDSet); + return eventData; + } +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/TableMapEventDataDeserializer.java b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/TableMapEventDataDeserializer.java index 35ca1edf..6cb814cb 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/TableMapEventDataDeserializer.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/TableMapEventDataDeserializer.java @@ -46,6 +46,7 @@ public TableMapEventData deserialize(ByteArrayInputStream inputStream) throws IO if (metadataLength > 0) { metadata = metadataDeserializer.deserialize( new ByteArrayInputStream(inputStream.read(metadataLength)), + eventData.getColumnTypes().length, numericColumnCount(eventData.getColumnTypes()) ); } diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/TableMapEventMetadataDeserializer.java b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/TableMapEventMetadataDeserializer.java index e175f444..c6efdda4 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/TableMapEventMetadataDeserializer.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/TableMapEventMetadataDeserializer.java @@ -26,13 +26,17 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; /** * @author Ahmed Abdul Hamid */ public class TableMapEventMetadataDeserializer { - public TableMapEventMetadata deserialize(ByteArrayInputStream inputStream, int nIntColumns) throws IOException { + private final Logger logger = Logger.getLogger(getClass().getName()); + + public TableMapEventMetadata deserialize(ByteArrayInputStream inputStream, int nColumns, int nNumericColumns) throws IOException { int remainingBytes = inputStream.available(); if (remainingBytes <= 0) { return null; @@ -41,7 +45,25 @@ public TableMapEventMetadata deserialize(ByteArrayInputStream inputStream, int n TableMapEventMetadata result = new TableMapEventMetadata(); for (; remainingBytes > 0; inputStream.enterBlock(remainingBytes)) { - MetadataFieldType fieldType = MetadataFieldType.byCode(inputStream.readInteger(1)); + int code = inputStream.readInteger(1); + + MetadataFieldType fieldType = MetadataFieldType.byCode(code); + if (fieldType == null) { + throw new IOException("Unsupported table metadata field type " + code); + } + if (MetadataFieldType.UNKNOWN_METADATA_FIELD_TYPE.equals(fieldType)) { + if (logger.isLoggable(Level.FINE)) { + logger.fine("Received metadata field of unknown type"); + } + continue; + } + + //for some reasons, the UNKNOWN_METADATA_FIELD_TYPE will mess up the stream + if(inputStream.available() == 0) { + logger.warning("Stream is empty so cannot read field length for field type: " + fieldType); + return result; + } + int fieldLength = inputStream.readPackedInteger(); remainingBytes = inputStream.available(); @@ -49,7 +71,7 @@ public TableMapEventMetadata deserialize(ByteArrayInputStream inputStream, int n switch (fieldType) { case SIGNEDNESS: - result.setSignedness(readSignedness(inputStream, nIntColumns)); + result.setSignedness(readBooleanList(inputStream, nNumericColumns)); break; case DEFAULT_CHARSET: result.setDefaultCharset(readDefaultCharset(inputStream)); @@ -81,16 +103,19 @@ public TableMapEventMetadata deserialize(ByteArrayInputStream inputStream, int n case ENUM_AND_SET_COLUMN_CHARSET: result.setEnumAndSetColumnCharsets(readIntegers(inputStream)); break; + case VISIBILITY: + result.setVisibility(readBooleanList(inputStream, nColumns)); + break; default: inputStream.enterBlock(remainingBytes); - throw new IOException("Unsupported table metadata field type " + fieldType); + throw new IOException("Unsupported table metadata field type " + code); } remainingBytes -= fieldLength; } return result; } - private static BitSet readSignedness(ByteArrayInputStream inputStream, int length) throws IOException { + private static BitSet readBooleanList(ByteArrayInputStream inputStream, int length) throws IOException { BitSet result = new BitSet(); // according to MySQL internals the amount of storage required for N columns is INT((N+7)/8) bytes byte[] bytes = inputStream.read((length + 7) >> 3); @@ -162,7 +187,9 @@ private enum MetadataFieldType { SIMPLE_PRIMARY_KEY(8), // The primary key without any prefix PRIMARY_KEY_WITH_PREFIX(9), // The primary key with some prefix ENUM_AND_SET_DEFAULT_CHARSET(10), // Charsets of ENUM and SET columns - ENUM_AND_SET_COLUMN_CHARSET(11); // Charsets of ENUM and SET columns + ENUM_AND_SET_COLUMN_CHARSET(11), // Charsets of ENUM and SET columns + VISIBILITY(12), // Column visibility (8.0.23 and newer) + UNKNOWN_METADATA_FIELD_TYPE(128); // Returned with binlog-row-metadata=FULL from MySQL 8.0 in some cases private final int code; diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/TransactionPayloadEventDataDeserializer.java b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/TransactionPayloadEventDataDeserializer.java new file mode 100644 index 00000000..a8e84876 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/TransactionPayloadEventDataDeserializer.java @@ -0,0 +1,102 @@ +/* + * Copyright 2013 Stanley Shyiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.shyiko.mysql.binlog.event.deserialization; + +import com.github.luben.zstd.Zstd; +import com.github.shyiko.mysql.binlog.event.Event; +import com.github.shyiko.mysql.binlog.event.TransactionPayloadEventData; +import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; + +/** + * @author Somesh Malviya + * @author Debjeet Sarkar + */ +public class TransactionPayloadEventDataDeserializer implements EventDataDeserializer { + public static final int OTW_PAYLOAD_HEADER_END_MARK = 0; + public static final int OTW_PAYLOAD_SIZE_FIELD = 1; + public static final int OTW_PAYLOAD_COMPRESSION_TYPE_FIELD = 2; + public static final int OTW_PAYLOAD_UNCOMPRESSED_SIZE_FIELD = 3; + + @Override + public TransactionPayloadEventData deserialize(ByteArrayInputStream inputStream) throws IOException { + TransactionPayloadEventData eventData = new TransactionPayloadEventData(); + // Read the header fields from the event data + while (inputStream.available() > 0) { + int fieldType = 0; + int fieldLen = 0; + // Read the type of the field + if (inputStream.available() >= 1) { + fieldType = inputStream.readPackedInteger(); + } + // We have reached the end of the Event Data Header + if (fieldType == OTW_PAYLOAD_HEADER_END_MARK) { + break; + } + // Read the size of the field + if (inputStream.available() >= 1) { + fieldLen = inputStream.readPackedInteger(); + } + switch (fieldType) { + case OTW_PAYLOAD_SIZE_FIELD: + // Fetch the payload size + eventData.setPayloadSize(inputStream.readPackedInteger()); + break; + case OTW_PAYLOAD_COMPRESSION_TYPE_FIELD: + // Fetch the compression type + eventData.setCompressionType(inputStream.readPackedInteger()); + break; + case OTW_PAYLOAD_UNCOMPRESSED_SIZE_FIELD: + // Fetch the uncompressed size + eventData.setUncompressedSize(inputStream.readPackedInteger()); + break; + default: + // Ignore unrecognized field + inputStream.read(fieldLen); + break; + } + } + if (eventData.getUncompressedSize() == 0) { + // Default the uncompressed to the payload size + eventData.setUncompressedSize(eventData.getPayloadSize()); + } + // set the payload to the rest of the input buffer + eventData.setPayload(inputStream.read(eventData.getPayloadSize())); + + // Decompress the payload + byte[] src = eventData.getPayload(); + byte[] dst = ByteBuffer.allocate(eventData.getUncompressedSize()).array(); + Zstd.decompressByteArray(dst, 0, dst.length, src, 0, src.length); + + // Read and store events from decompressed byte array into input stream + ArrayList decompressedEvents = new ArrayList<>(); + EventDeserializer transactionPayloadEventDeserializer = new EventDeserializer(); + ByteArrayInputStream destinationInputStream = new ByteArrayInputStream(dst); + + Event internalEvent = transactionPayloadEventDeserializer.nextEvent(destinationInputStream); + while(internalEvent != null) { + decompressedEvents.add(internalEvent); + internalEvent = transactionPayloadEventDeserializer.nextEvent(destinationInputStream); + } + + eventData.setUncompressedEvents(decompressedEvents); + + return eventData; + } +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonBinary.java b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonBinary.java index a6b5fc0e..8a2b8d84 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonBinary.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonBinary.java @@ -80,8 +80,6 @@ *

Grammar

* The grammar of the binary representation of JSON objects are defined in the MySQL codebase in the * json_binary.h file: - *

- * *

  *   doc ::= type value
  *   type ::=
@@ -164,11 +162,21 @@ public class JsonBinary {
      * @throws IOException if there is a problem reading or processing the binary representation
      */
     public static String parseAsString(byte[] bytes) throws IOException {
+        /* check for mariaDB-format JSON strings inside columns marked JSON */
+        if ( isJSONString(bytes) ) {
+            return new String(bytes);
+        }
         JsonStringFormatter handler = new JsonStringFormatter();
         parse(bytes, handler);
         return handler.getString();
     }
 
+    private static boolean isJSONString(byte[] bytes) {
+        if (bytes[0] > 0x0f)
+            return true;
+        else
+            return false;
+    }
     /**
      * Parse the MySQL binary representation of a {@code JSON} value and call the supplied {@link JsonFormatter}
      * for the various components of the value.
@@ -189,6 +197,7 @@ public JsonBinary(byte[] bytes) {
 
     public JsonBinary(ByteArrayInputStream contents) {
         this.reader = contents;
+        this.reader.mark(Integer.MAX_VALUE);
     }
 
     public String getString() {
@@ -262,8 +271,6 @@ protected void parse(ValueType type, JsonFormatter formatter) throws IOException
      * json_binary.h file:
      * 

Grammar

* - *

Grammar

- * *
      *   value ::=
      *       object  |
@@ -319,16 +326,21 @@ protected void parse(ValueType type, JsonFormatter formatter) throws IOException
      */
     protected void parseObject(boolean small, JsonFormatter formatter)
             throws IOException {
+        // this is terrible, but without a decent seekable InputStream the other way seemed like
+        // a full-on rewrite
+        int objectOffset = this.reader.getPosition();
+
         // Read the header ...
         int numElements = readUnsignedIndex(Integer.MAX_VALUE, small, "number of elements in");
         int numBytes = readUnsignedIndex(Integer.MAX_VALUE, small, "size of");
         int valueSize = small ? 2 : 4;
 
         // Read each key-entry, consisting of the offset and length of each key ...
-        int[] keyLengths = new int[numElements];
+        KeyEntry[] keys = new KeyEntry[numElements];
         for (int i = 0; i != numElements; ++i) {
-            readUnsignedIndex(numBytes, small, "key offset in"); // unused
-            keyLengths[i] = readUInt16();
+            keys[i] = new KeyEntry(
+                    readUnsignedIndex(numBytes, small, "key offset in"),
+                    readUInt16());
         }
 
         // Read each key value value-entry
@@ -373,9 +385,14 @@ protected void parseObject(boolean small, JsonFormatter formatter)
         }
 
         // Read each key ...
-        String[] keys = new String[numElements];
         for (int i = 0; i != numElements; ++i) {
-            keys[i] = reader.readString(keyLengths[i]);
+            final int skipBytes = keys[i].index + objectOffset - reader.getPosition();
+            // Skip to a start of a field name if the current position does not point to it
+            // This can happen for MySQL 8
+            if (skipBytes != 0) {
+                reader.fastSkip(skipBytes);
+            }
+            keys[i].name = reader.readString(keys[i].length);
         }
 
         // Now parse the values ...
@@ -384,7 +401,7 @@ protected void parseObject(boolean small, JsonFormatter formatter)
             if (i != 0) {
                 formatter.nextEntry();
             }
-            formatter.name(keys[i]);
+            formatter.name(keys[i].name);
             ValueEntry entry = entries[i];
             if (entry.resolved) {
                 Object value = entry.value;
@@ -397,6 +414,8 @@ protected void parseObject(boolean small, JsonFormatter formatter)
                 }
             } else {
                 // Parse the value ...
+                this.reader.reset();
+                this.reader.fastSkip(objectOffset + entry.index);
                 parse(entry.type, formatter);
             }
         }
@@ -463,6 +482,8 @@ protected void parseObject(boolean small, JsonFormatter formatter)
     // checkstyle, please ignore MethodLength for the next line
     protected void parseArray(boolean small, JsonFormatter formatter)
             throws IOException {
+        int arrayOffset = this.reader.getPosition();
+
         // Read the header ...
         int numElements = readUnsignedIndex(Integer.MAX_VALUE, small, "number of elements in");
         int numBytes = readUnsignedIndex(Integer.MAX_VALUE, small, "size of");
@@ -527,6 +548,9 @@ protected void parseArray(boolean small, JsonFormatter formatter)
                 }
             } else {
                 // Parse the value ...
+                this.reader.reset();
+                this.reader.fastSkip(arrayOffset + entry.index);
+
                 parse(entry.type, formatter);
             }
         }
@@ -650,7 +674,6 @@ protected void parseString(JsonFormatter formatter) throws IOException {
      * See the 
      * MySQL source code for the logic used in this method.
-     * 

*

Grammar

* *
@@ -946,6 +969,7 @@ protected BigInteger readUInt64() throws IOException {
      * to 16383, and so on...
      *
      * @return the integer value
+	 * @throws IOException if we don't encounter an end-of-int marker
      */
     protected int readVariableInt() throws IOException {
         int length = 0;
@@ -988,6 +1012,26 @@ protected static String asHex(int value) {
         return Integer.toHexString(value);
     }
 
+    /**
+     * Class used internally to hold key entry information.
+     */
+    protected static final class KeyEntry {
+
+        protected final int index;
+        protected final int length;
+        protected String name;
+
+        public KeyEntry(int index, int length) {
+            this.index = index;
+            this.length = length;
+        }
+
+        public KeyEntry setKey(String key) {
+            this.name = key;
+            return this;
+        }
+    }
+
     /**
      * Class used internally to hold value entry information.
      */
diff --git a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonStringFormatter.java b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonStringFormatter.java
index 5ae92c85..58a3b841 100644
--- a/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonStringFormatter.java
+++ b/src/main/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonStringFormatter.java
@@ -19,6 +19,7 @@
 
 import java.math.BigDecimal;
 import java.math.BigInteger;
+import java.util.Base64;
 
 /**
  * A {@link JsonFormatter} implementation that creates a JSON string representation.
@@ -197,7 +198,7 @@ public void valueTimestamp(long secondsPastEpoch, int microSeconds) {
     @Override
     public void valueOpaque(ColumnType type, byte[] value) {
         sb.append('"');
-        sb.append(javax.xml.bind.DatatypeConverter.printBase64Binary(value));
+        sb.append(Base64.getEncoder().encodeToString(value));
         sb.append('"');
     }
 
diff --git a/src/main/java/com/github/shyiko/mysql/binlog/io/BufferedSocketInputStream.java b/src/main/java/com/github/shyiko/mysql/binlog/io/BufferedSocketInputStream.java
index 69f00e1a..5095637c 100644
--- a/src/main/java/com/github/shyiko/mysql/binlog/io/BufferedSocketInputStream.java
+++ b/src/main/java/com/github/shyiko/mysql/binlog/io/BufferedSocketInputStream.java
@@ -60,6 +60,10 @@ public int read(byte[] b, int off, int len) throws IOException {
             }
             offset = 0;
             limit = in.read(buffer, 0, buffer.length);
+
+            if (limit == -1) {
+                return -1;
+            }
         }
         int bytesRemainingInBuffer = Math.min(len, limit - offset);
         System.arraycopy(buffer, offset, b, off, bytesRemainingInBuffer);
diff --git a/src/main/java/com/github/shyiko/mysql/binlog/io/ByteArrayInputStream.java b/src/main/java/com/github/shyiko/mysql/binlog/io/ByteArrayInputStream.java
index 350b8709..66dbe13a 100644
--- a/src/main/java/com/github/shyiko/mysql/binlog/io/ByteArrayInputStream.java
+++ b/src/main/java/com/github/shyiko/mysql/binlog/io/ByteArrayInputStream.java
@@ -27,10 +27,13 @@ public class ByteArrayInputStream extends InputStream {
 
     private InputStream inputStream;
     private Integer peek;
+    private Integer pos, markPosition;
     private int blockLength = -1;
+    private int initialBlockLength = -1;
 
     public ByteArrayInputStream(InputStream inputStream) {
         this.inputStream = inputStream;
+        this.pos = 0;
     }
 
     public ByteArrayInputStream(byte[] bytes) {
@@ -39,6 +42,9 @@ public ByteArrayInputStream(byte[] bytes) {
 
     /**
      * Read int written in little-endian format.
+	 * @param length length of the integer to read
+	 * @throws IOException in case of EOF
+	 * @return the integer from the binlog
      */
     public int readInteger(int length) throws IOException {
         int result = 0;
@@ -50,6 +56,9 @@ public int readInteger(int length) throws IOException {
 
     /**
      * Read long written in little-endian format.
+	 * @param length length of the long to read
+	 * @throws IOException in case of EOF
+	 * @return the long from the binlog
      */
     public long readLong(int length) throws IOException {
         long result = 0;
@@ -61,6 +70,9 @@ public long readLong(int length) throws IOException {
 
     /**
      * Read fixed length string.
+	 * @param length length of string to read
+	 * @throws IOException in case of EOF
+	 * @return string
      */
     public String readString(int length) throws IOException {
         return new String(read(length));
@@ -68,6 +80,8 @@ public String readString(int length) throws IOException {
 
     /**
      * Read variable-length string. Preceding packed integer indicates the length of the string.
+	 * @throws IOException in case of EOF
+	 * @return string
      */
     public String readLengthEncodedString() throws IOException {
         return readString(readPackedInteger());
@@ -75,6 +89,8 @@ public String readLengthEncodedString() throws IOException {
 
     /**
      * Read variable-length string. End is indicated by 0x00 byte.
+	 * @throws IOException in case of EOF
+	 * @return string
      */
     public String readZeroTerminatedString() throws IOException {
         ByteArrayOutputStream s = new ByteArrayOutputStream();
@@ -95,7 +111,10 @@ public void fill(byte[] bytes, int offset, int length) throws IOException {
         while (remaining != 0) {
             int read = read(bytes, offset + length - remaining, remaining);
             if (read == -1) {
-                throw new EOFException();
+                throw new EOFException(
+                    String.format("Failed to read remaining %d of %d bytes from position %d. Block length: %d. Initial block length: %d.",
+                        remaining, length, pos, blockLength, initialBlockLength)
+                );
             }
             remaining -= read;
         }
@@ -126,6 +145,8 @@ private byte[] reverse(byte[] bytes) {
 
     /**
      * @see #readPackedNumber()
+	 * @throws IOException in case of malformed number, eof, null, or long
+	 * @return integer
      */
     public int readPackedInteger() throws IOException {
         Number number = readPackedNumber();
@@ -139,12 +160,14 @@ public int readPackedInteger() throws IOException {
     }
 
     /**
-     * Format (first-byte-based):
- * 0-250 - The first byte is the number (in the range 0-250). No additional bytes are used.
- * 251 - SQL NULL value
- * 252 - Two more bytes are used. The number is in the range 251-0xffff.
- * 253 - Three more bytes are used. The number is in the range 0xffff-0xffffff.
+ * Format (first-byte-based):
+ * 0-250 - The first byte is the number (in the range 0-250). No additional bytes are used.
+ * 251 - SQL NULL value
+ * 252 - Two more bytes are used. The number is in the range 251-0xffff.
+ * 253 - Three more bytes are used. The number is in the range 0xffff-0xffffff.
* 254 - Eight more bytes are used. The number is in the range 0xffffff-0xffffffffffffffff. + * @throws IOException in case of malformed number or EOF + * @return long or null */ public Number readPackedNumber() throws IOException { int b = this.read(); @@ -187,8 +210,9 @@ public int read() throws IOException { peek = null; } if (result == -1) { - throw new EOFException(); + throw new EOFException(String.format("Failed to read next byte from position %d", this.pos)); } + this.pos += 1; return result; } @@ -202,6 +226,50 @@ private int readWithinBlockBoundaries() throws IOException { return inputStream.read(); } + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } + + if (peek != null) { + b[off] = (byte)(int)peek; + off += 1; + len -= 1; + } + + int read = readWithinBlockBoundaries(b, off, len); + + if (read > 0) { + this.pos += read; + } + + if (peek != null) { + peek = null; + read = read <= 0 ? 1 : read + 1; + } + + return read; + } + + private int readWithinBlockBoundaries(byte[] b, int off, int len) throws IOException { + if (blockLength == -1) { + return inputStream.read(b, off, len); + } else if (blockLength == 0) { + return -1; + } + + int read = inputStream.read(b, off, Math.min(len, blockLength)); + if (read > 0) { + blockLength -= read; + } + return read; + } + @Override public void close() throws IOException { inputStream.close(); @@ -209,6 +277,7 @@ public void close() throws IOException { public void enterBlock(int length) { this.blockLength = length < -1 ? -1 : length; + this.initialBlockLength = length; } public void skipToTheEndOfTheBlock() throws IOException { @@ -218,4 +287,46 @@ public void skipToTheEndOfTheBlock() throws IOException { } } + public int getPosition() { + return pos; + } + + @Override + public synchronized void mark(int readlimit) { + markPosition = pos; + inputStream.mark(readlimit); + } + + @Override + public boolean markSupported() { + return inputStream.markSupported(); + } + + @Override + public synchronized void reset() throws IOException { + pos = markPosition; + inputStream.reset(); + } + + /** + * This method implements fast-forward skipping in the stream. + * It can be used if and only if the underlying stream is fully available till its end. + * In other cases the regular {@link #skip(long)} method must be used. + * + * @param n - number of bytes to skip + * @return number of bytes skipped + * @throws IOException + */ + public synchronized long fastSkip(long n) throws IOException { + long skipOf = n; + if (blockLength != -1) { + skipOf = Math.min(blockLength, skipOf); + blockLength -= skipOf; + if (blockLength == 0) { + blockLength = -1; + } + } + pos += (int) skipOf; + return inputStream.skip(skipOf); + } } diff --git a/src/main/java/com/github/shyiko/mysql/binlog/io/ByteArrayOutputStream.java b/src/main/java/com/github/shyiko/mysql/binlog/io/ByteArrayOutputStream.java index 91e4ca44..17840c6f 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/io/ByteArrayOutputStream.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/io/ByteArrayOutputStream.java @@ -35,6 +35,9 @@ public ByteArrayOutputStream(OutputStream outputStream) { /** * Write int in little-endian format. + * @throws IOException on underlying stream error + * @param value integer to write + * @param length length in bytes of the integer */ public void writeInteger(int value, int length) throws IOException { for (int i = 0; i < length; i++) { @@ -44,6 +47,9 @@ public void writeInteger(int value, int length) throws IOException { /** * Write long in little-endian format. + * @throws IOException on underlying stream error + * @param value long to write + * @param length length in bytes of the long */ public void writeLong(long value, int length) throws IOException { for (int i = 0; i < length; i++) { @@ -57,9 +63,13 @@ public void writeString(String value) throws IOException { /** * @see ByteArrayInputStream#readZeroTerminatedString() + * @param value string to write + * @throws IOException on underlying stream error */ public void writeZeroTerminatedString(String value) throws IOException { - write(value.getBytes()); + if ( value != null ) + write(value.getBytes()); + write(0); } @@ -68,6 +78,11 @@ public void write(int b) throws IOException { outputStream.write(b); } + @Override + public void write(byte[] bytes) throws IOException { + outputStream.write(bytes); + } + public byte[] toByteArray() { // todo: whole approach feels wrong if (outputStream instanceof java.io.ByteArrayOutputStream) { diff --git a/src/main/java/com/github/shyiko/mysql/binlog/jmx/BinaryLogClientStatistics.java b/src/main/java/com/github/shyiko/mysql/binlog/jmx/BinaryLogClientStatistics.java index b1910901..dd13946f 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/jmx/BinaryLogClientStatistics.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/jmx/BinaryLogClientStatistics.java @@ -18,6 +18,7 @@ import com.github.shyiko.mysql.binlog.BinaryLogClient; import com.github.shyiko.mysql.binlog.event.Event; import com.github.shyiko.mysql.binlog.event.EventHeader; +import com.github.shyiko.mysql.binlog.event.EventType; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -65,6 +66,9 @@ public long getSecondsBehindMaster() { if (timestamp == 0 || eventHeader == null) { return -1; } + if (eventHeader.getEventType() == EventType.HEARTBEAT && eventHeader.getTimestamp() == 0) { + return 0; + } return (timestamp - eventHeader.getTimestamp()) / 1000; } diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/Authenticator.java b/src/main/java/com/github/shyiko/mysql/binlog/network/Authenticator.java new file mode 100644 index 00000000..0faf012d --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/Authenticator.java @@ -0,0 +1,179 @@ +package com.github.shyiko.mysql.binlog.network; + +import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; +import com.github.shyiko.mysql.binlog.io.ByteArrayOutputStream; +import com.github.shyiko.mysql.binlog.network.protocol.ErrorPacket; +import com.github.shyiko.mysql.binlog.network.protocol.GreetingPacket; +import com.github.shyiko.mysql.binlog.network.protocol.PacketChannel; +import com.github.shyiko.mysql.binlog.network.protocol.command.AuthenticateNativePasswordCommand; +import com.github.shyiko.mysql.binlog.network.protocol.command.AuthenticateSHA2Command; +import com.github.shyiko.mysql.binlog.network.protocol.command.AuthenticateSHA2RSAPasswordCommand; +import com.github.shyiko.mysql.binlog.network.protocol.command.AuthenticateSecurityPasswordCommand; +import com.github.shyiko.mysql.binlog.network.protocol.command.ByteArrayCommand; +import com.github.shyiko.mysql.binlog.network.protocol.command.Command; +import com.github.shyiko.mysql.binlog.network.protocol.command.SSLRequestCommand; + +import java.io.IOException; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class Authenticator { + private enum AuthMethod { + NATIVE, + CACHING_SHA2 + }; + + private final GreetingPacket greetingPacket; + private String scramble; + private final PacketChannel channel; + private final String schema; + private final String username; + private final String password; + + private final Logger logger = Logger.getLogger(getClass().getName()); + + private final String SHA2_PASSWORD = "caching_sha2_password"; + private final String MYSQL_NATIVE = "mysql_native_password"; + + private AuthMethod authMethod = AuthMethod.NATIVE; + + public Authenticator( + GreetingPacket greetingPacket, + PacketChannel channel, + String schema, + String username, + String password + ) { + this.greetingPacket = greetingPacket; + this.scramble = greetingPacket.getScramble(); + this.channel = channel; + this.schema = schema; + this.username = username; + this.password = password; + } + + public void authenticate() throws IOException { + logger.log(Level.FINE, "Begin auth for " + username); + int collation = greetingPacket.getServerCollation(); + + Command authenticateCommand; + if ( SHA2_PASSWORD.equals(greetingPacket.getPluginProvidedData()) ) { + authMethod = AuthMethod.CACHING_SHA2; + authenticateCommand = new AuthenticateSHA2Command(schema, username, password, scramble, collation); + } else { + authMethod = AuthMethod.NATIVE; + authenticateCommand = new AuthenticateSecurityPasswordCommand(schema, username, password, scramble, collation); + } + + channel.write(authenticateCommand); + readResult(); + logger.log(Level.FINE, "Auth complete " + username); + } + + private void readResult() throws IOException { + byte[] authenticationResult = channel.read(); + switch(authenticationResult[0]) { + case (byte) 0x00: + // success + return; + case (byte) 0xFF: + // error + byte[] bytes = Arrays.copyOfRange(authenticationResult, 1, authenticationResult.length); + ErrorPacket errorPacket = new ErrorPacket(bytes); + throw new AuthenticationException(errorPacket.getErrorMessage(), errorPacket.getErrorCode(), + errorPacket.getSqlState()); + case (byte) 0xFE: + switchAuthentication(authenticationResult); + return; + default: + if ( authMethod == AuthMethod.NATIVE ) + throw new AuthenticationException("Unexpected authentication result (" + authenticationResult[0] + ")"); + else + processCachingSHA2Result(authenticationResult); + } + } + + private void processCachingSHA2Result(byte[] authenticationResult) throws IOException { + if (authenticationResult.length < 2) + throw new AuthenticationException("caching_sha2_password response too short!"); + + ByteArrayInputStream stream = new ByteArrayInputStream(authenticationResult); + stream.readPackedInteger(); // throw away length, always 1 + + switch(stream.read()) { + case 0x03: + logger.log(Level.FINE, "cached sha2 auth successful"); + // successful fast authentication + readResult(); + return; + case 0x04: + logger.log(Level.FINE, "cached sha2 auth not successful, moving to full auth path"); + continueCachingSHA2Authentication(); + } + } + + private void continueCachingSHA2Authentication() throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + if ( channel.isSSL() ) { + // over SSL we simply send the password in cleartext. + + buffer.writeZeroTerminatedString(password); + + Command c = new ByteArrayCommand(buffer.toByteArray()); + channel.write(c); + readResult(); + } else { + // try to download an RSA key + buffer.write(0x02); + channel.write(new ByteArrayCommand(buffer.toByteArray())); + + ByteArrayInputStream stream = new ByteArrayInputStream(channel.read()); + int result = stream.read(); + switch(result) { + case 0x01: + byte[] rsaKey = new byte[stream.available()]; + stream.read(rsaKey); + + logger.log(Level.FINE, "received RSA key: " + rsaKey); + Command c = new AuthenticateSHA2RSAPasswordCommand(new String(rsaKey), password, scramble); + channel.write(c); + + readResult(); + return; + default: + throw new AuthenticationException("Unkown response fetching RSA key in caching_sha2_pasword auth: " + result); + } + } + } + + private void switchAuthentication(byte[] authenticationResult) throws IOException { + /* + Azure-MySQL likes to tell us to switch authentication methods, even though + we haven't advertised that we support any. It uses this for some-odd + reason to send the real password scramble. + */ + ByteArrayInputStream buffer = new ByteArrayInputStream(authenticationResult); + buffer.read(1); + + String authName = buffer.readZeroTerminatedString(); + if (MYSQL_NATIVE.equals(authName)) { + authMethod = AuthMethod.NATIVE; + + this.scramble = buffer.readZeroTerminatedString(); + + Command switchCommand = new AuthenticateNativePasswordCommand(scramble, password); + channel.write(switchCommand); + } else if ( SHA2_PASSWORD.equals(authName) ) { + authMethod = AuthMethod.CACHING_SHA2; + + this.scramble = buffer.readZeroTerminatedString(); + Command authCommand = new AuthenticateSHA2Command(scramble, password); + channel.write(authCommand); + } else { + throw new AuthenticationException("unsupported authentication method: " + authName); + } + + readResult(); + } +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/ClientCapabilities.java b/src/main/java/com/github/shyiko/mysql/binlog/network/ClientCapabilities.java index c744d5a9..d81e2e1a 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/network/ClientCapabilities.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/ClientCapabilities.java @@ -42,6 +42,7 @@ public final class ClientCapabilities { public static final int MULTI_RESULTS = 1 << 17; /* enable/disable multi-results */ public static final int PS_MULTI_RESULTS = 1 << 18; /* multi-results in ps-protocol */ public static final int PLUGIN_AUTH = 1 << 19; /* client supports plugin authentication */ + public static final int PLUGIN_AUTH_LENENC_CLIENT_DATA = 1 << 21; public static final int SSL_VERIFY_SERVER_CERT = 1 << 30; public static final int REMEMBER_OPTIONS = 1 << 31; diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/DefaultSSLSocketFactory.java b/src/main/java/com/github/shyiko/mysql/binlog/network/DefaultSSLSocketFactory.java index 0fabaa10..388e95d2 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/network/DefaultSSLSocketFactory.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/DefaultSSLSocketFactory.java @@ -30,11 +30,11 @@ public class DefaultSSLSocketFactory implements SSLSocketFactory { private final String protocol; public DefaultSSLSocketFactory() { - this("TLSv1"); + this("TLSv1.2"); } /** - * @param protocol TLSv1, TLSv1.1 or TLSv1.2 (the last two require JDK 7+) + * @param protocol TLSv1, TLSv1.1 or TLSv1.2. Since JDK 11.0.11, TLSv1 and TLSv1.1 are no longer supported. */ public DefaultSSLSocketFactory(String protocol) { this.protocol = protocol; diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/ErrorCode.java b/src/main/java/com/github/shyiko/mysql/binlog/network/ErrorCode.java index e7e46752..2cd4484e 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/network/ErrorCode.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/ErrorCode.java @@ -2178,7 +2178,7 @@ public final class ErrorCode { public static final int ER_TOO_BIG_PRECISION = 1426; /** - * For float(M,D), double(M,D) or decimal(M,D), M must be >= D (column '%-.192s'). + * For float(M,D), double(M,D) or decimal(M,D), M must be >= D (column '%-.192s'). */ public static final int ER_M_BIGGER_THAN_D = 1427; @@ -3146,7 +3146,7 @@ public final class ErrorCode { public static final int WARN_NO_MASTER_INFO = 1617; /** - * <%-.64s> option ignored + * %-.64s option ignored */ public static final int WARN_OPTION_IGNORED = 1618; @@ -3988,56 +3988,56 @@ public final class ErrorCode { public static final int ER_CANT_DO_IMPLICIT_COMMIT_IN_TRX_WHEN_GTID_NEXT_IS_SET = 1778; /** - * @@GLOBAL.GTID_MODE = ON or UPGRADE_STEP_2 requires @@GLOBAL.ENFORCE_GTID_CONSISTENCY = 1. + @@GLOBAL.GTID_MODE = ON or UPGRADE_STEP_2 requires @@GLOBAL.ENFORCE_GTID_CONSISTENCY = 1. */ public static final int ER_GTID_MODE_2_OR_3_REQUIRES_ENFORCE_GTID_CONSISTENCY_ON = 1779; /** - * @@GLOBAL.GTID_MODE = ON or UPGRADE_STEP_1 or UPGRADE_STEP_2 requires --log-bin and --log-slave-updates. + * @@GLOBAL.GTID_MODE = ON or UPGRADE_STEP_1 or UPGRADE_STEP_2 requires --log-bin and --log-slave-updates. */ public static final int ER_GTID_MODE_REQUIRES_BINLOG = 1780; /** - * @@SESSION.GTID_NEXT cannot be set to UUID:NUMBER when @@GLOBAL.GTID_MODE = OFF. + * @@SESSION.GTID_NEXT cannot be set to UUID:NUMBER when @@GLOBAL.GTID_MODE = OFF. */ public static final int ER_CANT_SET_GTID_NEXT_TO_GTID_WHEN_GTID_MODE_IS_OFF = 1781; /** - * @@SESSION.GTID_NEXT cannot be set to ANONYMOUS when @@GLOBAL.GTID_MODE = ON. + * @@SESSION.GTID_NEXT cannot be set to ANONYMOUS when @@GLOBAL.GTID_MODE = ON. */ public static final int ER_CANT_SET_GTID_NEXT_TO_ANONYMOUS_WHEN_GTID_MODE_IS_ON = 1782; /** - * @@SESSION.GTID_NEXT_LIST cannot be set to a non-NULL value when @@GLOBAL.GTID_MODE = OFF. + * @@SESSION.GTID_NEXT_LIST cannot be set to a non-NULL value when @@GLOBAL.GTID_MODE = OFF. */ public static final int ER_CANT_SET_GTID_NEXT_LIST_TO_NON_NULL_WHEN_GTID_MODE_IS_OFF = 1783; /** - * Found a Gtid_log_event or Previous_gtids_log_event when @@GLOBAL.GTID_MODE = OFF. + * Found a Gtid_log_event or Previous_gtids_log_event when @@GLOBAL.GTID_MODE = OFF. */ public static final int ER_FOUND_GTID_EVENT_WHEN_GTID_MODE_IS_OFF = 1784; /** - * When @@GLOBAL.ENFORCE_GTID_CONSISTENCY = 1, updates to non-transactional tables can only be done in either + * When @@GLOBAL.ENFORCE_GTID_CONSISTENCY = 1, updates to non-transactional tables can only be done in either * autocommitted statements or single-statement transactions, and never in the same statement as updates to * transactional tables. */ public static final int ER_GTID_UNSAFE_NON_TRANSACTIONAL_TABLE = 1785; /** - * CREATE TABLE ... SELECT is forbidden when @@GLOBAL.ENFORCE_GTID_CONSISTENCY = 1. + * CREATE TABLE ... SELECT is forbidden when @@GLOBAL.ENFORCE_GTID_CONSISTENCY = 1. */ public static final int ER_GTID_UNSAFE_CREATE_SELECT = 1786; /** - * When @@GLOBAL.ENFORCE_GTID_CONSISTENCY = 1, the statements CREATE TEMPORARY TABLE and DROP TEMPORARY TABLE can + * When @@GLOBAL.ENFORCE_GTID_CONSISTENCY = 1, the statements CREATE TEMPORARY TABLE and DROP TEMPORARY TABLE can * be executed in a non-transactional context only, and require that AUTOCOMMIT = 1. */ public static final int ER_GTID_UNSAFE_CREATE_DROP_TEMPORARY_TABLE_IN_TRANSACTION = 1787; /** - * The value of @@GLOBAL.GTID_MODE can only change one step at a time: OFF <-> UPGRADE_STEP_1 <-> UPGRADE_STEP_2 - * <-> ON. Also note that this value must be stepped up or down simultaneously on all servers; see the Manual for + * The value of @@GLOBAL.GTID_MODE can only change one step at a time: OFF <-> UPGRADE_STEP_1 <-> UPGRADE_STEP_2 + * <-> ON. Also note that this value must be stepped up or down simultaneously on all servers; see the Manual for * instructions. */ public static final int ER_GTID_MODE_CAN_ONLY_CHANGE_ONE_STEP_AT_A_TIME = 1788; @@ -4049,7 +4049,7 @@ public final class ErrorCode { public static final int ER_MASTER_HAS_PURGED_REQUIRED_GTIDS = 1789; /** - * @@SESSION.GTID_NEXT cannot be changed by a client that owns a GTID. The client owns %s. Ownership is released + * @@SESSION.GTID_NEXT cannot be changed by a client that owns a GTID. The client owns %s. Ownership is released * on COMMIT or ROLLBACK. */ public static final int ER_CANT_SET_GTID_NEXT_WHEN_OWNING_GTID = 1790; @@ -4291,8 +4291,8 @@ public final class ErrorCode { public static final int ER_READ_ONLY_MODE = 1836; /** - * When @@SESSION.GTID_NEXT is set to a GTID, you must explicitly set it to a different value after a COMMIT or - * ROLLBACK. Please check GTID_NEXT variable manual page for detailed explanation. Current @@SESSION.GTID_NEXT is + * When @@SESSION.GTID_NEXT is set to a GTID, you must explicitly set it to a different value after a COMMIT or + * ROLLBACK. Please check GTID_NEXT variable manual page for detailed explanation. Current @@SESSION.GTID_NEXT is * '%s'. */ public static final int ER_GTID_NEXT_TYPE_UNDEFINED_GROUP = 1837; @@ -4303,27 +4303,27 @@ public final class ErrorCode { public static final int ER_VARIABLE_NOT_SETTABLE_IN_SP = 1838; /** - * @@GLOBAL.GTID_PURGED can only be set when @@GLOBAL.GTID_MODE = ON. + * @@GLOBAL.GTID_PURGED can only be set when @@GLOBAL.GTID_MODE = ON. */ public static final int ER_CANT_SET_GTID_PURGED_WHEN_GTID_MODE_IS_OFF = 1839; /** - * @@GLOBAL.GTID_PURGED can only be set when @@GLOBAL.GTID_EXECUTED is empty. + * @@GLOBAL.GTID_PURGED can only be set when @@GLOBAL.GTID_EXECUTED is empty. */ public static final int ER_CANT_SET_GTID_PURGED_WHEN_GTID_EXECUTED_IS_NOT_EMPTY = 1840; /** - * @@GLOBAL.GTID_PURGED can only be set when there are no ongoing transactions (not even in other clients). + * @@GLOBAL.GTID_PURGED can only be set when there are no ongoing transactions (not even in other clients). */ public static final int ER_CANT_SET_GTID_PURGED_WHEN_OWNED_GTIDS_IS_NOT_EMPTY = 1841; /** - * @@GLOBAL.GTID_PURGED was changed from '%s' to '%s'. + * @@GLOBAL.GTID_PURGED was changed from '%s' to '%s'. */ public static final int ER_GTID_PURGED_WAS_CHANGED = 1842; /** - * @@GLOBAL.GTID_EXECUTED was changed from '%s' to '%s'. + * @@GLOBAL.GTID_EXECUTED was changed from '%s' to '%s'. */ public static final int ER_GTID_EXECUTED_WAS_CHANGED = 1843; @@ -4518,7 +4518,7 @@ public final class ErrorCode { public static final int ER_OLD_TEMPORALS_UPGRADED = 1880; /** - * Operation not allowed when innodb_forced_recovery > 0. + * Operation not allowed when innodb_forced_recovery > 0. */ public static final int ER_INNODB_FORCED_RECOVERY = 1881; diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/HostnameChecker.java b/src/main/java/com/github/shyiko/mysql/binlog/network/HostnameChecker.java new file mode 100644 index 00000000..059a5f33 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/HostnameChecker.java @@ -0,0 +1,581 @@ +/* + * $HeadURL: file:///opt/dev/not-yet-commons-ssl-SVN-repo/tags/commons-ssl-0.3.17/src/java/org/apache/commons/ssl/HostnameVerifier.java $ + * $Revision: 121 $ + * $Date: 2007-11-13 21:26:57 -0800 (Tue, 13 Nov 2007) $ + * + * ==================================================================== + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +/* + * after looking around the landscape of java verifying a certificate hostname, the best I found + * was to copy and paste from https://github.com/narupley/not-going-to-be-commons-ssl/blob/master/src/main/java/org/apache/commons/ssl/Certificates.java + * + * given that it seems like it's never going to be maintained upstream... + * is it bad? time will tell. + */ + +package com.github.shyiko.mysql.binlog.network; + +import javax.naming.InvalidNameException; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.security.auth.x500.X500Principal; +import java.io.IOException; +import java.io.InputStream; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.TreeSet; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Interface for checking if a hostname matches the names stored inside the + * server's X.509 certificate. Correctly implements + * javax.net.ssl.HostnameVerifier, but that interface is not recommended. + * Instead we added several check() methods that take SSLSocket, + * or X509Certificate, or ultimately (they all end up calling this one), + * String. (It's easier to supply JUnit with Strings instead of mock + * SSLSession objects!) + *

Our check() methods throw exceptions if the name is + * invalid, whereas javax.net.ssl.HostnameVerifier just returns true/false. + *

+ * We provide the HostnameVerifier.DEFAULT, HostnameVerifier.STRICT, and + * HostnameVerifier.ALLOW_ALL implementations. We also provide the more + * specialized HostnameVerifier.DEFAULT_AND_LOCALHOST, as well as + * HostnameVerifier.STRICT_IE6. But feel free to define your own + * implementations! + *

+ * Inspired by Sebastian Hauer's original StrictSSLProtocolSocketFactory in the + * HttpClient "contrib" repository. + * + * @author Julius Davies + * @author Sebastian Hauer + * @since 8-Dec-2006 + */ +public interface HostnameChecker extends javax.net.ssl.HostnameVerifier { + + boolean verify(String host, SSLSession session); + + void check(String host, SSLSocket ssl) throws IOException; + + void check(String host, X509Certificate cert) throws SSLException; + + void check(String host, String[] cns, String[] subjectAlts) + throws SSLException; + + void check(String[] hosts, SSLSocket ssl) throws IOException; + + void check(String[] hosts, X509Certificate cert) throws SSLException; + + + /** + * Checks to see if the supplied hostname matches any of the supplied CNs + * or "DNS" Subject-Alts. Most implementations only look at the first CN, + * and ignore any additional CNs. Most implementations do look at all of + * the "DNS" Subject-Alts. The CNs or Subject-Alts may contain wildcards + * according to RFC 2818. + * + * @param cns CN fields, in order, as extracted from the X.509 + * certificate. + * @param subjectAlts Subject-Alt fields of type 2 ("DNS"), as extracted + * from the X.509 certificate. + * @param hosts The array of hostnames to verify. + * @throws SSLException If verification failed. + */ + void check(String[] hosts, String[] cns, String[] subjectAlts) + throws SSLException; + + + /** + * The DEFAULT HostnameVerifier works the same way as Curl and Firefox. + *

+ * The hostname must match either the first CN, or any of the subject-alts. + * A wildcard can occur in the CN, and in any of the subject-alts. + *

+ * The only difference between DEFAULT and STRICT is that a wildcard (such + * as "*.foo.com") with DEFAULT matches all subdomains, including + * "a.b.foo.com". + */ + public final static HostnameChecker DEFAULT = + new AbstractChecker() { + public final void check(final String[] hosts, final String[] cns, + final String[] subjectAlts) + throws SSLException { + check(hosts, cns, subjectAlts, false, false); + } + + public final String toString() { return "DEFAULT"; } + }; + + + /** + * The DEFAULT_AND_LOCALHOST HostnameVerifier works like the DEFAULT + * one with one additional relaxation: a host of "localhost", + * "localhost.localdomain", "127.0.0.1", "::1" will always pass, no matter + * what is in the server's certificate. + */ + public final static HostnameChecker DEFAULT_AND_LOCALHOST = + new AbstractChecker() { + public final void check(final String[] hosts, final String[] cns, + final String[] subjectAlts) + throws SSLException { + if (isLocalhost(hosts[0])) { + return; + } + check(hosts, cns, subjectAlts, false, false); + } + + public final String toString() { return "DEFAULT_AND_LOCALHOST"; } + }; + + /** + * The STRICT HostnameVerifier works the same way as java.net.URL in Sun + * Java 1.4, Sun Java 5, Sun Java 6. It's also pretty close to IE6. + * This implementation appears to be compliant with RFC 2818 for dealing + * with wildcards. + *

+ * The hostname must match either the first CN, or any of the subject-alts. + * A wildcard can occur in the CN, and in any of the subject-alts. The + * one divergence from IE6 is how we only check the first CN. IE6 allows + * a match against any of the CNs present. We decided to follow in + * Sun Java 1.4's footsteps and only check the first CN. + *

+ * A wildcard such as "*.foo.com" matches only subdomains in the same + * level, for example "a.foo.com". It does not match deeper subdomains + * such as "a.b.foo.com". + */ + public final static HostnameChecker STRICT = + new AbstractChecker() { + public final void check(final String[] host, final String[] cns, + final String[] subjectAlts) + throws SSLException { + check(host, cns, subjectAlts, false, true); + } + + public final String toString() { return "STRICT"; } + }; + + /** + * The STRICT_IE6 HostnameVerifier works just like the STRICT one with one + * minor variation: the hostname can match against any of the CN's in the + * server's certificate, not just the first one. This behaviour is + * identical to IE6's behaviour. + */ + public final static HostnameChecker STRICT_IE6 = + new AbstractChecker() { + public final void check(final String[] host, final String[] cns, + final String[] subjectAlts) + throws SSLException { + check(host, cns, subjectAlts, true, true); + } + + public final String toString() { return "STRICT_IE6"; } + }; + + /** + * The ALLOW_ALL HostnameVerifier essentially turns hostname verification + * off. This implementation is a no-op, and never throws the SSLException. + */ + public final static HostnameChecker ALLOW_ALL = + new AbstractChecker() { + public final void check(final String[] host, final String[] cns, + final String[] subjectAlts) { + // Allow everything - so never blowup. + } + + public final String toString() { return "ALLOW_ALL"; } + }; + + abstract class AbstractChecker implements HostnameChecker { + private final Logger logger = Logger.getLogger(getClass().getName()); + + public static String[] getCNs(X509Certificate cert) { + try { + final String subjectPrincipal = cert.getSubjectX500Principal().getName(X500Principal.RFC2253); + final LinkedList cnList = new LinkedList(); + final LdapName subjectDN = new LdapName(subjectPrincipal); + for (final Rdn rds : subjectDN.getRdns()) { + final Attributes attributes = rds.toAttributes(); + final Attribute cn = attributes.get("cn"); + if (cn != null) { + try { + final Object value = cn.get(); + if (value != null) { + cnList.add(value.toString()); + } + } catch (NoSuchElementException ignore) { + } catch (NamingException ignore) { + } + } + } + if (!cnList.isEmpty()) { + return cnList.toArray(new String[cnList.size()]); + } + } catch (InvalidNameException ignore) { + } + return null; + } + + /** + * Extracts the array of SubjectAlt DNS names from an X509Certificate. + * Returns null if there aren't any. + *

+ * Note: Java doesn't appear able to extract international characters + * from the SubjectAlts. It can only extract international characters + * from the CN field. + *

+ * (Or maybe the version of OpenSSL I'm using to test isn't storing the + * international characters correctly in the SubjectAlts?). + * + * @param cert X509Certificate + * @return Array of SubjectALT DNS names stored in the certificate. + */ + public static String[] getDNSSubjectAlts(X509Certificate cert) { + LinkedList subjectAltList = new LinkedList(); + Collection c = null; + try { + c = cert.getSubjectAlternativeNames(); + } + catch (Exception cpe) { } + if (c != null) { + Iterator it = c.iterator(); + while (it.hasNext()) { + List list = (List) it.next(); + int type = ((Integer) list.get(0)).intValue(); + // If type is 2, then we've got a dNSName + if (type == 2) { + String s = (String) list.get(1); + subjectAltList.add(s); + } + } + } + if (!subjectAltList.isEmpty()) { + String[] subjectAlts = new String[subjectAltList.size()]; + subjectAltList.toArray(subjectAlts); + return subjectAlts; + } else { + return null; + } + } + + /** + * This contains a list of 2nd-level domains that aren't allowed to + * have wildcards when combined with country-codes. + * For example: [*.co.uk]. + *

+ * The [*.co.uk] problem is an interesting one. Should we just hope + * that CA's would never foolishly allow such a certificate to happen? + * Looks like we're the only implementation guarding against this. + * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check. + */ + private final static String[] BAD_COUNTRY_2LDS = + {"ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info", + "lg", "ne", "net", "or", "org"}; + + private final static String[] LOCALHOSTS = {"::1", "127.0.0.1", + "localhost", + "localhost.localdomain"}; + + + static { + // Just in case developer forgot to manually sort the array. :-) + Arrays.sort(BAD_COUNTRY_2LDS); + Arrays.sort(LOCALHOSTS); + } + + protected AbstractChecker() {} + + /** + * The javax.net.ssl.HostnameVerifier contract. + * + * @param host 'hostname' we used to create our socket + * @param session SSLSession with the remote server + * @return true if the host matched the one in the certificate. + */ + public boolean verify(String host, SSLSession session) { + try { + Certificate[] certs = session.getPeerCertificates(); + X509Certificate x509 = (X509Certificate) certs[0]; + check(new String[]{host}, x509); + return true; + } + catch (SSLException e) { + return false; + } + } + + public void check(String host, SSLSocket ssl) throws IOException { + check(new String[]{host}, ssl); + } + + public void check(String host, X509Certificate cert) + throws SSLException { + check(new String[]{host}, cert); + } + + public void check(String host, String[] cns, String[] subjectAlts) + throws SSLException { + check(new String[]{host}, cns, subjectAlts); + } + + public void check(String host[], SSLSocket ssl) + throws IOException { + if (host == null) { + throw new NullPointerException("host to verify is null"); + } + + SSLSession session = ssl.getSession(); + if (session == null) { + // In our experience this only happens under IBM 1.4.x when + // spurious (unrelated) certificates show up in the server' + // chain. Hopefully this will unearth the real problem: + InputStream in = ssl.getInputStream(); + in.available(); + /* + If you're looking at the 2 lines of code above because + you're running into a problem, you probably have two + options: + + #1. Clean up the certificate chain that your server + is presenting (e.g. edit "/etc/apache2/server.crt" + or wherever it is your server's certificate chain + is defined). + + OR + + #2. Upgrade to an IBM 1.5.x or greater JVM, or switch + to a non-IBM JVM. + */ + + // If ssl.getInputStream().available() didn't cause an + // exception, maybe at least now the session is available? + session = ssl.getSession(); + if (session == null) { + // If it's still null, probably a startHandshake() will + // unearth the real problem. + ssl.startHandshake(); + + // Okay, if we still haven't managed to cause an exception, + // might as well go for the NPE. Or maybe we're okay now? + session = ssl.getSession(); + } + } + Certificate[] certs; + try { + certs = session.getPeerCertificates(); + } catch (SSLPeerUnverifiedException spue) { + InputStream in = ssl.getInputStream(); + in.available(); + // Didn't trigger anything interesting? Okay, just throw + // original. + throw spue; + } + X509Certificate x509 = (X509Certificate) certs[0]; + check(host, x509); + } + + private String commaJoin(String [] input) { + if ( input == null ) return ""; + return String.join(",", Arrays.asList(input)); + } + + public void check(String[] host, X509Certificate cert) + throws SSLException { + String[] cns = AbstractChecker.getCNs(cert); + String[] subjectAlts = AbstractChecker.getDNSSubjectAlts(cert); + logger.log(Level.INFO, + "attempting to verify SSL identity '" + commaJoin(host) + "' " + + "against cns: [" + commaJoin(cns) + "], " + + "subject-alts: [" + commaJoin(subjectAlts) + "]"); + check(host, cns, subjectAlts); + } + + public void check(final String[] hosts, final String[] cns, + final String[] subjectAlts, final boolean ie6, + final boolean strictWithSubDomains) + throws SSLException { + // Build up lists of allowed hosts For logging/debugging purposes. + StringBuffer buf = new StringBuffer(32); + buf.append('<'); + for (int i = 0; i < hosts.length; i++) { + String h = hosts[i]; + h = h != null ? h.trim().toLowerCase() : ""; + hosts[i] = h; + if (i > 0) { + buf.append('/'); + } + buf.append(h); + } + buf.append('>'); + String hostnames = buf.toString(); + // Build the list of names we're going to check. Our DEFAULT and + // STRICT implementations of the HostnameVerifier only use the + // first CN provided. All other CNs are ignored. + // (Firefox, wget, curl, Sun Java 1.4, 5, 6 all work this way). + TreeSet names = new TreeSet(); + if (cns != null && cns.length > 0 && cns[0] != null) { + names.add(cns[0]); + if (ie6) { + for (int i = 1; i < cns.length; i++) { + names.add(cns[i]); + } + } + } + if (subjectAlts != null) { + for (int i = 0; i < subjectAlts.length; i++) { + if (subjectAlts[i] != null) { + names.add(subjectAlts[i]); + } + } + } + if (names.isEmpty()) { + String msg = "Certificate for " + hosts[0] + " doesn't contain CN or DNS subjectAlt"; + throw new SSLException(msg); + } + + // StringBuffer for building the error message. + buf = new StringBuffer(); + + boolean match = false; + out: + for (Iterator it = names.iterator(); it.hasNext();) { + // Don't trim the CN, though! + String cn = (String) it.next(); + cn = cn.toLowerCase(); + // Store CN in StringBuffer in case we need to report an error. + buf.append(" <"); + buf.append(cn); + buf.append('>'); + if (it.hasNext()) { + buf.append(" OR"); + } + + // The CN better have at least two dots if it wants wildcard + // action. It also can't be [*.co.uk] or [*.co.jp] or + // [*.org.uk], etc... + boolean doWildcard = cn.startsWith("*.") && + cn.lastIndexOf('.') >= 0 && + !isIP4Address(cn) && + acceptableCountryWildcard(cn); + + for (int i = 0; i < hosts.length; i++) { + final String hostName = hosts[i].trim().toLowerCase(); + if (doWildcard) { + match = hostName.endsWith(cn.substring(1)); + if (match && strictWithSubDomains) { + // If we're in strict mode, then [*.foo.com] is not + // allowed to match [a.b.foo.com] + match = countDots(hostName) == countDots(cn); + } + } else { + match = hostName.equals(cn); + } + if (match) { + break out; + } + } + } + if (!match) { + throw new SSLException("hostname in certificate didn't match: " + hostnames + " !=" + buf); + } + } + + public static boolean isIP4Address(final String cn) { + boolean isIP4 = true; + String tld = cn; + int x = cn.lastIndexOf('.'); + // We only bother analyzing the characters after the final dot + // in the name. + if (x >= 0 && x + 1 < cn.length()) { + tld = cn.substring(x + 1); + } + for (int i = 0; i < tld.length(); i++) { + if (!Character.isDigit(tld.charAt(0))) { + isIP4 = false; + break; + } + } + return isIP4; + } + + public static boolean acceptableCountryWildcard(final String cn) { + int cnLen = cn.length(); + if (cnLen >= 7 && cnLen <= 9) { + // Look for the '.' in the 3rd-last position: + if (cn.charAt(cnLen - 3) == '.') { + // Trim off the [*.] and the [.XX]. + String s = cn.substring(2, cnLen - 3); + // And test against the sorted array of bad 2lds: + int x = Arrays.binarySearch(BAD_COUNTRY_2LDS, s); + return x < 0; + } + } + return true; + } + + public static boolean isLocalhost(String host) { + host = host != null ? host.trim().toLowerCase() : ""; + if (host.startsWith("::1")) { + int x = host.lastIndexOf('%'); + if (x >= 0) { + host = host.substring(0, x); + } + } + int x = Arrays.binarySearch(LOCALHOSTS, host); + return x >= 0; + } + + /** + * Counts the number of dots "." in a string. + * + * @param s string to count dots from + * @return number of dots + */ + public static int countDots(final String s) { + int count = 0; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '.') { + count++; + } + } + return count; + } + } + +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/SSLMode.java b/src/main/java/com/github/shyiko/mysql/binlog/network/SSLMode.java index a5ce7f42..041dd6ea 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/network/SSLMode.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/SSLMode.java @@ -16,8 +16,7 @@ package com.github.shyiko.mysql.binlog.network; /** - * @see * ssl-mode for the original documentation. * @author Stanley Shyiko */ public enum SSLMode { diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/ServerException.java b/src/main/java/com/github/shyiko/mysql/binlog/network/ServerException.java index cfd026d2..03ce3b72 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/network/ServerException.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/ServerException.java @@ -33,6 +33,7 @@ public ServerException(String message, int errorCode, String sqlState) { /** * @see ErrorCode + * @return error code */ public int getErrorCode() { return errorCode; diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/TLSHostnameVerifier.java b/src/main/java/com/github/shyiko/mysql/binlog/network/TLSHostnameVerifier.java deleted file mode 100644 index 28b106d6..00000000 --- a/src/main/java/com/github/shyiko/mysql/binlog/network/TLSHostnameVerifier.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2016 Stanley Shyiko - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.github.shyiko.mysql.binlog.network; - -import sun.security.util.HostnameChecker; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -/** - * @author Stanley Shyiko - */ -public class TLSHostnameVerifier implements HostnameVerifier { - - public boolean verify(String hostname, SSLSession session) { - HostnameChecker checker = HostnameChecker.getInstance(HostnameChecker.TYPE_TLS); - try { - Certificate[] peerCertificates = session.getPeerCertificates(); - if (peerCertificates.length > 0 && peerCertificates[0] instanceof X509Certificate) { - X509Certificate peerCertificate = (X509Certificate) peerCertificates[0]; - try { - checker.match(hostname, peerCertificate); - return true; - } catch (CertificateException ignored) { - } - } - } catch (SSLPeerUnverifiedException ignored) { - } - return false; - } - -} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/PacketChannel.java b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/PacketChannel.java index fbbe950f..c64e7034 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/PacketChannel.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/PacketChannel.java @@ -32,7 +32,9 @@ * @author Stanley Shyiko */ public class PacketChannel implements Channel { - + private int packetNumber = 0; + private boolean authenticationComplete; + private boolean isSSL = false; private Socket socket; private ByteArrayInputStream inputStream; private ByteArrayOutputStream outputStream; @@ -55,36 +57,41 @@ public ByteArrayOutputStream getOutputStream() { return outputStream; } + public void authenticationComplete() { + authenticationComplete = true; + } + public byte[] read() throws IOException { int length = inputStream.readInteger(3); - inputStream.skip(1); //sequence + int sequence = inputStream.read(); // sequence + if ( sequence != packetNumber++ ) { + throw new IOException("unexpected sequence #" + sequence); + } return inputStream.read(length); } - public void write(Command command, int packetNumber) throws IOException { + public void write(Command command) throws IOException { byte[] body = command.toByteArray(); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); buffer.writeInteger(body.length, 3); // packet length - buffer.writeInteger(packetNumber, 1); + + // see https://dev.mysql.com/doc/dev/mysql-server/8.0.11/page_protocol_basic_packets.html#sect_protocol_basic_packets_sequence_id + // we only have to maintain a sequence number in the authentication phase. + // what the point is, I do not know + if ( authenticationComplete ) { + packetNumber = 0; + } + + buffer.writeInteger(packetNumber++, 1); + buffer.write(body, 0, body.length); + buffer.flush(); outputStream.write(buffer.toByteArray()); // though it has no effect in case of default (underlying) output stream (SocketOutputStream), // it may be necessary in case of non-default one outputStream.flush(); } - /** - * @deprecated use {@link #write(Command, int)} instead - */ - @Deprecated - public void writeBuffered(Command command, int packetNumber) throws IOException { - write(command, packetNumber); - } - - public void write(Command command) throws IOException { - write(command, 0); - } - public void upgradeToSSL(SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier) throws IOException { SSLSocket sslSocket = sslSocketFactory.createSocket(this.socket); sslSocket.startHandshake(); @@ -96,6 +103,11 @@ public void upgradeToSSL(SSLSocketFactory sslSocketFactory, HostnameVerifier hos throw new IdentityVerificationException("\"" + sslSocket.getInetAddress().getHostName() + "\" identity was not confirmed"); } + isSSL = true; + } + + public boolean isSSL() { + return isSSL; } @Override diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateNativePasswordCommand.java b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateNativePasswordCommand.java index f98eced0..25711af0 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateNativePasswordCommand.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateNativePasswordCommand.java @@ -29,6 +29,6 @@ public AuthenticateNativePasswordCommand(String scramble, String password) { } @Override public byte[] toByteArray() throws IOException { - return AuthenticateCommand.passwordCompatibleWithMySQL411(password, scramble); + return AuthenticateSecurityPasswordCommand.passwordCompatibleWithMySQL411(password, scramble); } } diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateSHA2Command.java b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateSHA2Command.java new file mode 100644 index 00000000..b776df58 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateSHA2Command.java @@ -0,0 +1,142 @@ +/* + * Copyright 2018 dingxiaobo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.shyiko.mysql.binlog.network.protocol.command; + +import com.github.shyiko.mysql.binlog.io.ByteArrayOutputStream; +import com.github.shyiko.mysql.binlog.network.ClientCapabilities; + +import java.io.IOException; +import java.security.DigestException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * @author dingxiaobo + */ +public class AuthenticateSHA2Command implements Command { + + private String schema; + private String username; + private String password; + private String scramble; + private int clientCapabilities; + private int collation; + private boolean rawPassword = false; + + public AuthenticateSHA2Command(String schema, String username, String password, String scramble, int collation) { + this.schema = schema; + this.username = username; + this.password = password; + this.scramble = scramble; + this.collation = collation; + } + + public AuthenticateSHA2Command(String scramble, String password) { + this.rawPassword = true; + this.password = password; + this.scramble = scramble; + } + + public void setClientCapabilities(int clientCapabilities) { + this.clientCapabilities = clientCapabilities; + } + + @Override + public byte[] toByteArray() throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + if ( rawPassword ) { + byte[] passwordSHA1 = encodePassword(); + buffer.write(passwordSHA1); + return buffer.toByteArray(); + } + + int clientCapabilities = this.clientCapabilities; + if (clientCapabilities == 0) { + clientCapabilities |= ClientCapabilities.LONG_FLAG; + clientCapabilities |= ClientCapabilities.PROTOCOL_41; + clientCapabilities |= ClientCapabilities.SECURE_CONNECTION; + clientCapabilities |= ClientCapabilities.PLUGIN_AUTH; + clientCapabilities |= ClientCapabilities.PLUGIN_AUTH_LENENC_CLIENT_DATA; + + if (schema != null) { + clientCapabilities |= ClientCapabilities.CONNECT_WITH_DB; + } + } + buffer.writeInteger(clientCapabilities, 4); + buffer.writeInteger(0, 4); // maximum packet length + buffer.writeInteger(collation, 1); + for (int i = 0; i < 23; i++) { + buffer.write(0); + } + buffer.writeZeroTerminatedString(username); + byte[] passwordSHA1 = encodePassword(); + buffer.writeInteger(passwordSHA1.length, 1); + buffer.write(passwordSHA1); + if (schema != null) { + buffer.writeZeroTerminatedString(schema); + } + buffer.writeZeroTerminatedString("caching_sha2_password"); + + return buffer.toByteArray(); + } + + private byte[] encodePassword() { + if (password == null || "".equals(password)) { + return new byte[0]; + } + // caching_sha2_password + /* + * Server does it in 4 steps (see sql/auth/sha2_password_common.cc Generate_scramble::scramble method): + * + * SHA2(src) => digest_stage1 + * SHA2(digest_stage1) => digest_stage2 + * SHA2(digest_stage2, m_rnd) => scramble_stage1 + * XOR(digest_stage1, scramble_stage1) => scramble + */ + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-256"); + + int CACHING_SHA2_DIGEST_LENGTH = 32; + byte[] dig1 = new byte[CACHING_SHA2_DIGEST_LENGTH]; + byte[] dig2 = new byte[CACHING_SHA2_DIGEST_LENGTH]; + byte[] scramble1 = new byte[CACHING_SHA2_DIGEST_LENGTH]; + + // SHA2(src) => digest_stage1 + md.update(password.getBytes(), 0, password.getBytes().length); + md.digest(dig1, 0, CACHING_SHA2_DIGEST_LENGTH); + md.reset(); + + // SHA2(digest_stage1) => digest_stage2 + md.update(dig1, 0, dig1.length); + md.digest(dig2, 0, CACHING_SHA2_DIGEST_LENGTH); + md.reset(); + + // SHA2(digest_stage2, m_rnd) => scramble_stage1 + md.update(dig2, 0, dig1.length); + md.update(scramble.getBytes(), 0, scramble.getBytes().length); + md.digest(scramble1, 0, CACHING_SHA2_DIGEST_LENGTH); + + // XOR(digest_stage1, scramble_stage1) => scramble + return CommandUtils.xor(dig1, scramble1); + } catch (NoSuchAlgorithmException ex) { + throw new RuntimeException(ex); + } catch (DigestException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateSHA2RSAPasswordCommand.java b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateSHA2RSAPasswordCommand.java new file mode 100644 index 00000000..e8de8d4f --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateSHA2RSAPasswordCommand.java @@ -0,0 +1,63 @@ +package com.github.shyiko.mysql.binlog.network.protocol.command; + +import com.github.shyiko.mysql.binlog.network.AuthenticationException; +import com.github.shyiko.mysql.binlog.io.ByteArrayOutputStream; + +import javax.crypto.Cipher; +import java.io.IOException; +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +public class AuthenticateSHA2RSAPasswordCommand implements Command { + private static final String RSA_METHOD = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + private final String rsaKey; + private final String password; + private final String scramble; + + public AuthenticateSHA2RSAPasswordCommand(String rsaKey, String password, String scramble) { + this.rsaKey = rsaKey; + this.password = password; + this.scramble = scramble; + } + + @Override + public byte[] toByteArray() throws IOException { + RSAPublicKey key = decodeKey(rsaKey); + + ByteArrayOutputStream passBuffer = new ByteArrayOutputStream(); + passBuffer.writeZeroTerminatedString(password); + + byte[] xorBuffer = CommandUtils.xor(passBuffer.toByteArray(), scramble.getBytes()); + return encrypt(xorBuffer, key, RSA_METHOD); + } + + private RSAPublicKey decodeKey(String key) throws AuthenticationException { + int beginIndex = key.indexOf("\n") + 1; + int endIndex = key.indexOf("-----END PUBLIC KEY-----"); + String innerKey = key.substring(beginIndex, endIndex).replaceAll("\\n", ""); + + Base64.Decoder decoder = Base64.getDecoder(); + byte[] certificateData = decoder.decode(innerKey.getBytes()); + + X509EncodedKeySpec spec = new X509EncodedKeySpec(certificateData); + try { + KeyFactory kf = KeyFactory.getInstance("RSA"); + return (RSAPublicKey) kf.generatePublic(spec); + } catch (Exception e) { + throw new AuthenticationException("Unable to decode public key: " + key); + } + } + + private byte[] encrypt(byte[] source, RSAPublicKey key, String transformation) throws AuthenticationException { + try { + Cipher cipher = Cipher.getInstance(transformation); + cipher.init(Cipher.ENCRYPT_MODE, key); + return cipher.doFinal(source); + } catch (Exception e) { + throw new AuthenticationException("couldn't encrypt password: " + e.getMessage()); + } + } + +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateCommand.java b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateSecurityPasswordCommand.java similarity index 78% rename from src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateCommand.java rename to src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateSecurityPasswordCommand.java index a045fe24..744e85fe 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateCommand.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/AuthenticateSecurityPasswordCommand.java @@ -25,7 +25,7 @@ /** * @author Stanley Shyiko */ -public class AuthenticateCommand implements Command { +public class AuthenticateSecurityPasswordCommand implements Command { private String schema; private String username; @@ -34,11 +34,12 @@ public class AuthenticateCommand implements Command { private int clientCapabilities; private int collation; - public AuthenticateCommand(String schema, String username, String password, String salt) { + public AuthenticateSecurityPasswordCommand(String schema, String username, String password, String salt, int collation) { this.schema = schema; this.username = username; this.password = password; this.salt = salt; + this.collation = collation; } public void setClientCapabilities(int clientCapabilities) { @@ -55,7 +56,10 @@ public byte[] toByteArray() throws IOException { int clientCapabilities = this.clientCapabilities; if (clientCapabilities == 0) { clientCapabilities = ClientCapabilities.LONG_FLAG | - ClientCapabilities.PROTOCOL_41 | ClientCapabilities.SECURE_CONNECTION; + ClientCapabilities.PROTOCOL_41 | + ClientCapabilities.SECURE_CONNECTION | + ClientCapabilities.PLUGIN_AUTH; + if (schema != null) { clientCapabilities |= ClientCapabilities.CONNECT_WITH_DB; } @@ -67,19 +71,26 @@ public byte[] toByteArray() throws IOException { buffer.write(0); } buffer.writeZeroTerminatedString(username); - byte[] passwordSHA1 = "".equals(password) ? new byte[0] : passwordCompatibleWithMySQL411(password, salt); + byte[] passwordSHA1 = passwordCompatibleWithMySQL411(password, salt); buffer.writeInteger(passwordSHA1.length, 1); buffer.write(passwordSHA1); if (schema != null) { buffer.writeZeroTerminatedString(schema); } + buffer.writeZeroTerminatedString("mysql_native_password"); return buffer.toByteArray(); } /** * see mysql/sql/password.c scramble(...) + * @param password the password + * @param salt salt received from server + * @return hashed password */ public static byte[] passwordCompatibleWithMySQL411(String password, String salt) { + if ( "".equals(password) || password == null ) + return new byte[0]; + MessageDigest sha; try { sha = MessageDigest.getInstance("SHA-1"); @@ -87,7 +98,7 @@ public static byte[] passwordCompatibleWithMySQL411(String password, String salt throw new RuntimeException(e); } byte[] passwordHash = sha.digest(password.getBytes()); - return xor(passwordHash, sha.digest(union(salt.getBytes(), sha.digest(passwordHash)))); + return CommandUtils.xor(passwordHash, sha.digest(union(salt.getBytes(), sha.digest(passwordHash)))); } private static byte[] union(byte[] a, byte[] b) { @@ -96,13 +107,4 @@ private static byte[] union(byte[] a, byte[] b) { System.arraycopy(b, 0, r, a.length, b.length); return r; } - - private static byte[] xor(byte[] a, byte[] b) { - byte[] r = new byte[a.length]; - for (int i = 0; i < r.length; i++) { - r[i] = (byte) (a[i] ^ b[i]); - } - return r; - } - } diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/ByteArrayCommand.java b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/ByteArrayCommand.java new file mode 100644 index 00000000..94ee6998 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/ByteArrayCommand.java @@ -0,0 +1,15 @@ +package com.github.shyiko.mysql.binlog.network.protocol.command; + +import java.io.IOException; + +public class ByteArrayCommand implements Command { + private final byte[] command; + + public ByteArrayCommand(byte[] command) { + this.command = command; + } + @Override + public byte[] toByteArray() throws IOException { + return command; + } +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/CommandUtils.java b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/CommandUtils.java new file mode 100644 index 00000000..68c09095 --- /dev/null +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/CommandUtils.java @@ -0,0 +1,12 @@ +package com.github.shyiko.mysql.binlog.network.protocol.command; + +public class CommandUtils { + public static byte[] xor(byte[] input, byte[] against) { + byte[] to = new byte[input.length]; + + for( int i = 0; i < input.length; i++ ) { + to[i] = (byte) (input[i] ^ against[i % against.length]); + } + return to; + } +} diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/DumpBinaryLogCommand.java b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/DumpBinaryLogCommand.java index 36216c76..b7436aab 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/DumpBinaryLogCommand.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/DumpBinaryLogCommand.java @@ -24,9 +24,11 @@ */ public class DumpBinaryLogCommand implements Command { + public static final int BINLOG_SEND_ANNOTATE_ROWS_EVENT = 2; private long serverId; private String binlogFilename; private long binlogPosition; + private boolean sendAnnotateRowsEvent; public DumpBinaryLogCommand(long serverId, String binlogFilename, long binlogPosition) { this.serverId = serverId; @@ -34,12 +36,21 @@ public DumpBinaryLogCommand(long serverId, String binlogFilename, long binlogPos this.binlogPosition = binlogPosition; } + public DumpBinaryLogCommand(long serverId, String binlogFilename, long binlogPosition, boolean sendAnnotateRowsEvent) { + this(serverId, binlogFilename, binlogPosition); + this.sendAnnotateRowsEvent = sendAnnotateRowsEvent; + } + @Override public byte[] toByteArray() throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); buffer.writeInteger(CommandType.BINLOG_DUMP.ordinal(), 1); buffer.writeLong(this.binlogPosition, 4); - buffer.writeInteger(0, 2); // flag + int binlogFlags = 0; + if (sendAnnotateRowsEvent) { + binlogFlags |= BINLOG_SEND_ANNOTATE_ROWS_EVENT; + } + buffer.writeInteger(binlogFlags, 2); // flag buffer.writeLong(this.serverId, 4); buffer.writeString(this.binlogFilename); return buffer.toByteArray(); diff --git a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/SSLRequestCommand.java b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/SSLRequestCommand.java index ea748104..959cf5f9 100644 --- a/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/SSLRequestCommand.java +++ b/src/main/java/com/github/shyiko/mysql/binlog/network/protocol/command/SSLRequestCommand.java @@ -42,7 +42,9 @@ public byte[] toByteArray() throws IOException { int clientCapabilities = this.clientCapabilities; if (clientCapabilities == 0) { clientCapabilities = ClientCapabilities.LONG_FLAG | - ClientCapabilities.PROTOCOL_41 | ClientCapabilities.SECURE_CONNECTION; + ClientCapabilities.PROTOCOL_41 | + ClientCapabilities.SECURE_CONNECTION | + ClientCapabilities.PLUGIN_AUTH; } clientCapabilities |= ClientCapabilities.SSL; buffer.writeInteger(clientCapabilities, 4); diff --git a/src/test/java/com/github/shyiko/mysql/binlog/AbstractIntegrationTest.java b/src/test/java/com/github/shyiko/mysql/binlog/AbstractIntegrationTest.java new file mode 100644 index 00000000..802052f6 --- /dev/null +++ b/src/test/java/com/github/shyiko/mysql/binlog/AbstractIntegrationTest.java @@ -0,0 +1,69 @@ +package com.github.shyiko.mysql.binlog; + +import com.github.shyiko.mysql.binlog.event.EventType; +import com.github.shyiko.mysql.binlog.event.deserialization.EventDeserializer; +import org.testng.annotations.BeforeClass; + +import java.sql.SQLException; +import java.sql.Statement; +import java.util.TimeZone; + +public abstract class AbstractIntegrationTest { + protected MySQLConnection master; + protected MySQLConnection slave; + protected BinaryLogClient client; + protected CountDownEventListener eventListener; + protected MysqlVersion mysqlVersion; + + protected MysqlOnetimeServerOptions getOptions() { + MysqlOnetimeServerOptions options = new MysqlOnetimeServerOptions(); + options.fullRowMetaData = true; + return options; + } + + @BeforeClass + public void setUp() throws Exception { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + mysqlVersion = MysqlOnetimeServer.getVersion(); + MysqlOnetimeServer masterServer = new MysqlOnetimeServer(getOptions()); + MysqlOnetimeServer slaveServer = new MysqlOnetimeServer(getOptions()); + + masterServer.boot(); + slaveServer.boot(); + slaveServer.setupSlave(masterServer.getPort()); + + master = new MySQLConnection("127.0.0.1", masterServer.getPort(), "root", ""); + slave = new MySQLConnection("127.0.0.1", slaveServer.getPort(), "root", ""); + + client = new BinaryLogClient(slave.hostname, slave.port, slave.username, slave.password); + EventDeserializer eventDeserializer = new EventDeserializer(); + eventDeserializer.setCompatibilityMode(EventDeserializer.CompatibilityMode.CHAR_AND_BINARY_AS_BYTE_ARRAY, + EventDeserializer.CompatibilityMode.DATE_AND_TIME_AS_LONG); + client.setEventDeserializer(eventDeserializer); + client.setServerId(client.getServerId() - 1); // avoid clashes between BinaryLogClient instances + client.setKeepAlive(false); + client.registerEventListener(new TraceEventListener()); + client.registerEventListener(eventListener = new CountDownEventListener()); + client.registerLifecycleListener(new TraceLifecycleListener()); + client.connect(BinaryLogClientIntegrationTest.DEFAULT_TIMEOUT); + master.execute(new BinaryLogClientIntegrationTest.Callback() { + @Override + public void execute(Statement statement) throws SQLException { + statement.execute("drop database if exists mbcj_test"); + statement.execute("create database mbcj_test"); + statement.execute("use mbcj_test"); + } + }); + eventListener.waitFor(EventType.QUERY, 2, BinaryLogClientIntegrationTest.DEFAULT_TIMEOUT); + + if ( mysqlVersion.atLeast(8, 0) ) { + setupMysql8Login(master); + eventListener.waitFor(EventType.QUERY, 2, BinaryLogClientIntegrationTest.DEFAULT_TIMEOUT); + } + } + + protected void setupMysql8Login(MySQLConnection server) throws Exception { + server.execute("create user 'mysql8' IDENTIFIED WITH caching_sha2_password BY 'testpass'"); + server.execute("grant replication slave, replication client on *.* to 'mysql8'"); + } +} diff --git a/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogClientGTIDIntegrationTest.java b/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogClientGTIDIntegrationTest.java index df033afa..5246bd65 100644 --- a/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogClientGTIDIntegrationTest.java +++ b/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogClientGTIDIntegrationTest.java @@ -18,6 +18,7 @@ import com.github.shyiko.mysql.binlog.event.QueryEventData; import com.github.shyiko.mysql.binlog.event.XidEventData; import com.github.shyiko.mysql.binlog.event.deserialization.EventDeserializer; +import org.testng.SkipException; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -34,56 +35,28 @@ * @author Ben Osheroff */ public class BinaryLogClientGTIDIntegrationTest extends BinaryLogClientIntegrationTest { - - @BeforeClass - private void enableGTID() throws SQLException { - MySQLConnection[] servers = {slave, master}; - for (MySQLConnection m : servers) { - m.execute(new Callback() { - @Override - public void execute(Statement statement) throws SQLException { - ResultSet rs = statement.executeQuery("select @@GLOBAL.GTID_MODE as gtid_mode"); - rs.next(); - if ("ON".equals(rs.getString("gtid_mode"))) { - return; - } - statement.execute("SET @@GLOBAL.ENFORCE_GTID_CONSISTENCY = ON;"); - statement.execute("SET @@GLOBAL.GTID_MODE = OFF_PERMISSIVE;"); - statement.execute("SET @@GLOBAL.GTID_MODE = ON_PERMISSIVE;"); - statement.execute("SET @@GLOBAL.GTID_MODE = ON;"); - } - }, true); + @Override + protected MysqlOnetimeServerOptions getOptions() { + if ( !this.mysqlVersion.atLeast(5,7) ) { + throw new SkipException("skipping gtid on 5.5"); } - } - @AfterClass(alwaysRun = true) - private void disableGTID() throws SQLException { - MySQLConnection[] servers = {slave, master}; - for (MySQLConnection m : servers) { - m.execute(new Callback() { - @Override - public void execute(Statement statement) throws SQLException { - statement.execute("SET @@GLOBAL.GTID_MODE = ON_PERMISSIVE;"); - statement.execute("SET @@GLOBAL.GTID_MODE = OFF_PERMISSIVE;"); - statement.execute("SET @@GLOBAL.GTID_MODE = OFF;"); - statement.execute("SET @@GLOBAL.ENFORCE_GTID_CONSISTENCY = OFF;"); - } - }, true); - } - slave.execute("STOP SLAVE", "START SLAVE"); + MysqlOnetimeServerOptions options = new MysqlOnetimeServerOptions(); + options.gtid = true; + return options; } @Test public void testGTIDAdvancesStatementBased() throws Exception { try { master.execute("set global binlog_format=statement"); - slave.execute("set global binlog_format=statement", "stop slave", "start slave"); + slave.execute("stop slave", "set global binlog_format=statement", "start slave"); master.reconnect(); master.execute("use test"); testGTIDAdvances(); } finally { master.execute("set global binlog_format=row"); - slave.execute("set global binlog_format=row", "stop slave", "start slave"); + slave.execute("stop slave", "set global binlog_format=row", "start slave"); master.reconnect(); master.execute("use test"); } diff --git a/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogClientIntegrationTest.java b/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogClientIntegrationTest.java index 71dfb524..f720e4ee 100644 --- a/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogClientIntegrationTest.java +++ b/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogClientIntegrationTest.java @@ -32,18 +32,16 @@ import com.github.shyiko.mysql.binlog.io.BufferedSocketInputStream; import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; import com.github.shyiko.mysql.binlog.network.AuthenticationException; +import com.github.shyiko.mysql.binlog.network.SSLMode; import com.github.shyiko.mysql.binlog.network.ServerException; import com.github.shyiko.mysql.binlog.network.SocketFactory; import org.mockito.InOrder; import org.testng.SkipException; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import javax.xml.bind.DatatypeConverter; -import java.io.Closeable; import java.io.EOFException; import java.io.FilterInputStream; import java.io.FilterOutputStream; @@ -55,13 +53,12 @@ import java.math.MathContext; import java.net.Socket; import java.net.SocketException; -import java.sql.Connection; -import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLSyntaxErrorException; import java.sql.Statement; import java.util.AbstractMap; +import java.util.Base64; import java.util.BitSet; import java.util.Calendar; import java.util.List; @@ -95,11 +92,11 @@ /** * @author Stanley Shyiko */ -public class BinaryLogClientIntegrationTest { +public class BinaryLogClientIntegrationTest extends AbstractIntegrationTest { protected static final long DEFAULT_TIMEOUT = TimeUnit.SECONDS.toMillis(3); - private final Logger logger = Logger.getLogger(getClass().getSimpleName()); + private final Logger logger = Logger.getLogger("donkey"); { logger.setLevel(Level.FINEST); @@ -107,43 +104,6 @@ public class BinaryLogClientIntegrationTest { private final TimeZone timeZoneBeforeTheTest = TimeZone.getDefault(); - protected MySQLConnection master, slave; - protected BinaryLogClient client; - protected CountDownEventListener eventListener; - - @BeforeClass - public void setUp() throws Exception { - TimeZone.setDefault(TimeZone.getTimeZone("GMT")); - ResourceBundle bundle = ResourceBundle.getBundle("jdbc"); - String prefix = "jdbc.mysql.replication."; - master = new MySQLConnection(bundle.getString(prefix + "master.hostname"), - Integer.parseInt(bundle.getString(prefix + "master.port")), - bundle.getString(prefix + "master.username"), bundle.getString(prefix + "master.password")); - slave = new MySQLConnection(bundle.getString(prefix + "slave.hostname"), - Integer.parseInt(bundle.getString(prefix + "slave.port")), - bundle.getString(prefix + "slave.superUsername"), bundle.getString(prefix + "slave.superPassword")); - client = new BinaryLogClient(slave.hostname, slave.port, slave.username, slave.password); - EventDeserializer eventDeserializer = new EventDeserializer(); - eventDeserializer.setCompatibilityMode(CompatibilityMode.CHAR_AND_BINARY_AS_BYTE_ARRAY, - CompatibilityMode.DATE_AND_TIME_AS_LONG); - client.setEventDeserializer(eventDeserializer); - client.setServerId(client.getServerId() - 1); // avoid clashes between BinaryLogClient instances - client.setKeepAlive(false); - client.registerEventListener(new TraceEventListener()); - client.registerEventListener(eventListener = new CountDownEventListener()); - client.registerLifecycleListener(new TraceLifecycleListener()); - client.connect(DEFAULT_TIMEOUT); - master.execute(new Callback() { - @Override - public void execute(Statement statement) throws SQLException { - statement.execute("drop database if exists mbcj_test"); - statement.execute("create database mbcj_test"); - statement.execute("use mbcj_test"); - } - }); - eventListener.waitFor(EventType.QUERY, 2, DEFAULT_TIMEOUT); - } - @BeforeMethod public void beforeEachTest() throws Exception { master.execute(new Callback() { @@ -153,7 +113,7 @@ public void execute(Statement statement) throws SQLException { statement.execute("create table bikini_bottom (name varchar(255) primary key)"); } }); - eventListener.waitFor(EventType.QUERY, 2, DEFAULT_TIMEOUT); + eventListener.waitForAtLeast(EventType.QUERY, 2, DEFAULT_TIMEOUT); eventListener.reset(); } @@ -322,7 +282,7 @@ public void testDeserializationOfSTRING() throws Exception { assertEquals(writeAndCaptureRow("binary", "x'01'"), new Serializable[]{new byte[] {1}}); assertEquals(writeAndCaptureRow("binary", "x'FF'"), new Serializable[]{new byte[] {-1}}); assertEquals(writeAndCaptureRow("binary(16)", "unhex(md5(\"glob\"))"), - new Serializable[]{DatatypeConverter.parseHexBinary("8684147451a6cc3b92142c6f4b78e61c")}); + new Serializable[]{Base64.getDecoder().decode("hoQUdFGmzDuSFCxvS3jmHA==")}); } @Test @@ -406,6 +366,51 @@ public void testDeserializationOfDateAndTimeAsLong() throws Exception { } } + @Test + public void testDeserializationOfIntegerAsByteArray() throws Exception { + final BinaryLogClient client = new BinaryLogClient(slave.hostname, slave.port, + slave.username, slave.password); + EventDeserializer eventDeserializer = new EventDeserializer(); + eventDeserializer.setCompatibilityMode(CompatibilityMode.INTEGER_AS_BYTE_ARRAY); + client.setEventDeserializer(eventDeserializer); + client.connect(DEFAULT_TIMEOUT); + try { + Serializable[] result; + + result = writeAndCaptureRow("tinyint unsigned", "0", "1", "255"); + assertEquals(result[0], 0); + assertEquals(result[1], 1); + assertEquals(result[2], -1); + + + result = writeAndCaptureRow("tinyint", "-128", "-1", "0", "1", "127"); + assertEquals(result[0], -128); + assertEquals(result[1], -1); + assertEquals(result[2], 0); + assertEquals(result[3], 1); + assertEquals(result[4], 127); + + result = writeAndCaptureRow("smallint unsigned", "0", "1", "65535"); + assertEquals(result[0], 0); + assertEquals(result[1], 1); + assertEquals(result[2], -1); + + result = writeAndCaptureRow("smallint", "-32768", "-1", "0", "1", "32767"); + assertEquals(result[0], -32768); + assertEquals(result[1], -1); + assertEquals(result[2], 0); + assertEquals(result[3], 1); + assertEquals(result[4], 32767); + + result = writeAndCaptureRow("mediumint unsigned", "0", "1", "16777215"); + assertEquals(result[0], 0); + assertEquals(result[1], 1); + assertEquals(result[2], -1); + } finally { + client.disconnect(); + } + } + @Test public void testDeserializationOfDateAndTimeAsLongMicrosecondsPrecision() throws Exception { final BinaryLogClient client = new BinaryLogClient(slave.hostname, slave.port, @@ -801,7 +806,7 @@ public void execute(Statement statement) throws SQLException { statement.execute("flush logs"); } }); - eventListener.waitFor(EventType.QUERY, 1, DEFAULT_TIMEOUT); + eventListener.waitForAtLeast(EventType.QUERY, 1, DEFAULT_TIMEOUT); eventListener.waitFor(EventType.ROTATE, 3, DEFAULT_TIMEOUT); /* 2 with timestamp 0 */ eventListener.waitFor(ByteArrayEventData.class, 5, DEFAULT_TIMEOUT); } finally { @@ -818,6 +823,12 @@ public void testExceptionIsThrownWhenTryingToConnectAlreadyConnectedClient() thr client.connect(); } + @Test(expectedExceptions = IOException.class) + public void testExceptionIsThrownWhenTryingToConnectAlreadyConnectedClientWithTimeout() throws Exception { + assertTrue(client.isConnected()); + client.connect(1000); + } + @Test public void testExceptionIsThrownWhenProvidedWithWrongCredentials() throws Exception { BinaryLogClient binaryLogClient = @@ -836,6 +847,7 @@ public void testExceptionIsThrownWhenInsufficientPermissionsToDetectPosition() t String prefix = "jdbc.mysql.replication."; String slaveUsername = bundle.getString(prefix + "slave.slaveUsername"); String slavePassword = bundle.getString(prefix + "slave.slavePassword"); + new BinaryLogClient(slave.hostname, slave.port, slaveUsername, slavePassword).connect(); } @@ -992,8 +1004,64 @@ public void execute(Statement statement) throws SQLException { } } + @Test + public void testMysql8Auth() throws Exception { + if ( !mysqlVersion.atLeast(8, 0) ) + throw new SkipException("skipping mysql8 auth test"); + + BinaryLogClient client = new BinaryLogClient(master.hostname, master.port, "mysql8", "testpass"); + client.setSSLMode(SSLMode.PREFERRED); + client.connect(DEFAULT_TIMEOUT); + } + + @Test + public void testMysql8FastAuth() throws Exception { + if ( !mysqlVersion.atLeast(8, 0) ) + throw new SkipException("skipping mysql8 auth test"); + + BinaryLogClient client = new BinaryLogClient(master.hostname, master.port, "mysql8", "testpass"); + client.setSSLMode(SSLMode.PREFERRED); + client.connect(DEFAULT_TIMEOUT); + + client.disconnect(); + + // this call should hit the sha2 cache + client.connect(DEFAULT_TIMEOUT); + } + + + @Test + public void testSHA2CachingAuthAsDefault() throws Exception { + if ( !mysqlVersion.atLeast(8, 0) ) + throw new SkipException("skipping mysql8 auth test"); + + MysqlOnetimeServerOptions opts = new MysqlOnetimeServerOptions(); + opts.extraParams = "--default-authentication-plugin=caching_sha2_password"; + MysqlOnetimeServer server = new MysqlOnetimeServer(opts); + server.boot(); + + MySQLConnection cx = new MySQLConnection("127.0.0.1", server.getPort(), "root", ""); + + setupMysql8Login(cx); + BinaryLogClient c = new BinaryLogClient(cx.hostname, cx.port, "mysql8", "testpass"); + c.setSSLMode(SSLMode.PREFERRED); + c.connect(DEFAULT_TIMEOUT); + + server.shutDown(); + } + + @Test + public void testSHA2CachingWithoutSSL() throws Exception { + if ( !mysqlVersion.atLeast(8, 0) ) + throw new SkipException("skipping mysql8 auth test"); + + BinaryLogClient client = new BinaryLogClient(master.hostname, master.port, "mysql8", "testpass"); + client.connect(DEFAULT_TIMEOUT); + } + @Test public void testMySQL8TableMetadata() throws Exception { + master.execute("drop table if exists test_metameta"); master.execute("create table test_metameta ( " + "a date, b date, c date, d date, e date, f date, g date, " + "h date, i date, j int)"); @@ -1001,6 +1069,32 @@ public void testMySQL8TableMetadata() throws Exception { eventListener.waitFor(WriteRowsEventData.class, 1, DEFAULT_TIMEOUT); } + @Test + public void testSetMasterServerId() throws Exception { + slave.query("SELECT @@server_id", new Callback() { + @Override + public void execute(final ResultSet rs) throws SQLException { + rs.next(); + assertEquals(client.getMasterServerId(), rs.getLong("@@server_id")); + } + }); + } + + @Test + public void testMySQL8InvisibleColumn() throws Exception { + if ( !mysqlVersion.atLeast(8, 0) ) + throw new SkipException("skipping mysql8 invisible column test"); + + master.execute("drop table if exists test_invisible_column"); + master.execute("create table test_invisible_column (\n" + + "id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n" + + "name varchar(100) not null,\n" + + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP INVISIBLE\n" + + ");"); + master.execute("insert into test_invisible_column (name) values ('User 1')"); + eventListener.waitFor(WriteRowsEventData.class, 1, DEFAULT_TIMEOUT); + } + @AfterMethod public void afterEachTest() throws Exception { final CountDownLatch latch = new CountDownLatch(1); @@ -1010,7 +1104,7 @@ public void afterEachTest() throws Exception { public void onEvent(Event event) { if (event.getHeader().getEventType() == EventType.QUERY) { EventData data = event.getData(); - if (data != null && ((QueryEventData) data).getSql().contains("_EOS_marker")) { + if (data != null && ((QueryEventData) data).getSql().toLowerCase().contains("_eos_marker")) { latch.countDown(); } } @@ -1051,114 +1145,6 @@ public void execute(Statement statement) throws SQLException { } } - /** - * Representation of a MySQL connection. - */ - public static final class MySQLConnection implements Closeable { - - private final String hostname; - private final int port; - private final String username; - private final String password; - private Connection connection; - - public MySQLConnection(String hostname, int port, String username, String password) - throws ClassNotFoundException, SQLException { - this.hostname = hostname; - this.port = port; - this.username = username; - this.password = password; - Class.forName("com.mysql.jdbc.Driver"); - connect(); - } - - private void connect() throws SQLException { - this.connection = DriverManager.getConnection("jdbc:mysql://" + hostname + ":" + port + - "?serverTimezone=UTC", username, password); - execute(new Callback() { - - @Override - public void execute(Statement statement) throws SQLException { - statement.execute("SET time_zone = '+00:00'"); - } - }); - } - - public String hostname() { - return hostname; - } - - public int port() { - return port; - } - - public String username() { - return username; - } - - public String password() { - return password; - } - - public void execute(Callback callback, boolean autocommit) throws SQLException { - connection.setAutoCommit(autocommit); - Statement statement = connection.createStatement(); - try { - callback.execute(statement); - if (!autocommit) { - connection.commit(); - } - } finally { - statement.close(); - } - } - - public void execute(Callback callback) throws SQLException { - execute(callback, false); - } - - public void execute(final String...statements) throws SQLException { - execute(new Callback() { - @Override - public void execute(Statement statement) throws SQLException { - for (String command : statements) { - statement.execute(command); - } - } - }); - } - - public void query(String sql, Callback callback) throws SQLException { - connection.setAutoCommit(false); - Statement statement = connection.createStatement(); - try { - ResultSet rs = statement.executeQuery(sql); - try { - callback.execute(rs); - connection.commit(); - } finally { - rs.close(); - } - } finally { - statement.close(); - } - } - - @Override - public void close() throws IOException { - try { - connection.close(); - } catch (SQLException e) { - throw new IOException(e); - } - } - - public void reconnect() throws IOException, SQLException { - close(); - connect(); - } - } - /** * Callback used in the {@link MySQLConnection#execute(Callback)} method. * diff --git a/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogClientTest.java b/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogClientTest.java index fece36b4..81660b71 100644 --- a/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogClientTest.java +++ b/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogClientTest.java @@ -49,7 +49,7 @@ public void testEventListenersManagement() { assertEquals(binaryLogClient.getEventListeners().size(), 3); binaryLogClient.unregisterEventListener(traceEventListener); assertEquals(binaryLogClient.getEventListeners().size(), 2); - binaryLogClient.unregisterEventListener(CountDownEventListener.class); + binaryLogClient.unregisterEventListener(CapturingEventListener.class); assertEquals(binaryLogClient.getEventListeners().size(), 1); } @@ -115,7 +115,7 @@ public void testNullEventDeserializerIsNotAllowed() throws Exception { @Test(timeOut = 15000) public void testDisconnectWhileBlockedByFBRead() throws Exception { - final BinaryLogClient binaryLogClient = new BinaryLogClient("localhost", 33060, "root", "mysql"); + final BinaryLogClient binaryLogClient = new BinaryLogClient("localhost", 33061, "root", "mysql"); final CountDownLatch readAttempted = new CountDownLatch(1); binaryLogClient.setSocketFactory(new SocketFactory() { @Override @@ -144,7 +144,7 @@ public void run() { try { final ServerSocket serverSocket = new ServerSocket(); try { - serverSocket.bind(new InetSocketAddress("localhost", 33060)); + serverSocket.bind(new InetSocketAddress("localhost", 33061)); socketBound.countDown(); serverSocket.accept(); // accept socket but do NOT send anything assertTrue(readAttempted.await(3000, TimeUnit.MILLISECONDS)); @@ -178,4 +178,19 @@ public void run() { } } + /* + @Test + public void testDeadlockyCode() throws IOException, InterruptedException { + final BinaryLogClient binaryLogClient = new BinaryLogClient("localhost", 3306, "root", "123456"); + binaryLogClient.setHeartbeatInterval(10000); + binaryLogClient.setKeepAlive(true); + binaryLogClient.setKeepAliveInterval(2000); + + binaryLogClient.connect(); + + Thread.sleep(1000); + + binaryLogClient.disconnect(); + } + */ } diff --git a/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogFileReaderIntegrationTest.java b/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogFileReaderIntegrationTest.java index cbf523a6..71c48eef 100644 --- a/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogFileReaderIntegrationTest.java +++ b/src/test/java/com/github/shyiko/mysql/binlog/BinaryLogFileReaderIntegrationTest.java @@ -30,6 +30,7 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; @@ -45,6 +46,13 @@ public void testNextEvent() throws Exception { readAll(reader, 1462); } + @Test + public void testNextEventCompressed() throws Exception { + BinaryLogFileReader reader = new BinaryLogFileReader( + new FileInputStream("src/test/resources/mysql-bin.compressed")); + readAll(reader, 5); + } + @Test public void testChecksumNONE() throws Exception { EventDeserializer eventDeserializer = new EventDeserializer(); @@ -70,6 +78,36 @@ public void testChecksumCRC32WithCustomEventDataDeserializer() throws Exception readAll(reader, 303); } + @Test + public void testUnsupportedEventType() throws Exception { + EventDeserializer eventDeserializer = new EventDeserializer(); + + // mysql> SHOW BINLOG EVENTS IN 'mysql-bin.aurora-padding'; + // +--------------------------+------+----------------+-------------+---------------------------------------+ + // | Log_name | Pos | Event_type | End_log_pos | Info | + // +--------------------------+------+----------------+-------------+---------------------------------------+ + // | mysql-bin.aurora-padding | 4 | Format_desc | 185 | Server ver: 5.7.12-log, Binlog ver: 4 | + // | mysql-bin.aurora-padding | 185 | Previous_gtids | 216 | | + // | mysql-bin.aurora-padding | 216 | Anonymous_Gtid | 281 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' | + // | mysql-bin.aurora-padding | 281 | Aurora_padding | 1209 | Ignorable | + // | mysql-bin.aurora-padding | 1209 | Query | 1294 | BEGIN | + BinaryLogFileReader reader = new BinaryLogFileReader( + new FileInputStream("src/test/resources/mysql-bin.aurora-padding"), eventDeserializer); + try { + for (int i = 0; i < 3; i++) { + assertNotNull(reader.readEvent()); + } + try { + reader.readEvent(); + } catch (IOException e) { + // this simulates the Debezium's event.processing.failure.handling.mode = warn + } + assertEquals(reader.readEvent().getHeader().getEventType(), EventType.QUERY); + } finally { + reader.close(); + } + } + private void readAll(BinaryLogFileReader reader, int expect) throws IOException { try { int numberOfEvents = 0; diff --git a/src/test/java/com/github/shyiko/mysql/binlog/CapturingEventListener.java b/src/test/java/com/github/shyiko/mysql/binlog/CapturingEventListener.java index f6b7c6b0..0249f6e1 100644 --- a/src/test/java/com/github/shyiko/mysql/binlog/CapturingEventListener.java +++ b/src/test/java/com/github/shyiko/mysql/binlog/CapturingEventListener.java @@ -25,7 +25,7 @@ /** * @author Stanley Shyiko */ -public class CapturingEventListener implements BinaryLogClient.EventListener { +public class CapturingEventListener extends CountDownEventListener { private final List events = new LinkedList(); @@ -33,6 +33,7 @@ public class CapturingEventListener implements BinaryLogClient.EventListener { public void onEvent(Event event) { synchronized (events) { events.add(event); + super.onEvent(event); } } diff --git a/src/test/java/com/github/shyiko/mysql/binlog/CountDownEventListener.java b/src/test/java/com/github/shyiko/mysql/binlog/CountDownEventListener.java index 73a6f5b9..6ceaa7d0 100644 --- a/src/test/java/com/github/shyiko/mysql/binlog/CountDownEventListener.java +++ b/src/test/java/com/github/shyiko/mysql/binlog/CountDownEventListener.java @@ -86,6 +86,23 @@ private void waitForCounterToGetZero(String counterName, AtomicInteger counter, } } + public void waitForAtLeast(EventType eventType, int numberOfEvents, long timeoutInMilliseconds) + throws TimeoutException, InterruptedException { + AtomicInteger counter = getCounter(countersByType, eventType); + + synchronized (counter) { + counter.set(counter.get() - numberOfEvents); + if (counter.get() < 0) { + counter.wait(timeoutInMilliseconds); + if (counter.get() < 0) { + throw new TimeoutException("Received " + (numberOfEvents + counter.get()) + " " + + eventType.name() + " event(s) instead of expected " + numberOfEvents); + } + } + counter.set(0); + } + } + public void reset() { synchronized (countersByType) { countersByType.clear(); diff --git a/src/test/java/com/github/shyiko/mysql/binlog/GtidSetTest.java b/src/test/java/com/github/shyiko/mysql/binlog/GtidSetTest.java index 79318aa6..cfb9f49e 100644 --- a/src/test/java/com/github/shyiko/mysql/binlog/GtidSetTest.java +++ b/src/test/java/com/github/shyiko/mysql/binlog/GtidSetTest.java @@ -164,4 +164,5 @@ public void testPutUUIDSet() { assertEquals(gtidSet, gtidSet2); } + } diff --git a/src/test/java/com/github/shyiko/mysql/binlog/MariadbBinaryLogClientIntegrationTest.java b/src/test/java/com/github/shyiko/mysql/binlog/MariadbBinaryLogClientIntegrationTest.java new file mode 100644 index 00000000..c021ec47 --- /dev/null +++ b/src/test/java/com/github/shyiko/mysql/binlog/MariadbBinaryLogClientIntegrationTest.java @@ -0,0 +1,102 @@ +package com.github.shyiko.mysql.binlog; + +import com.github.shyiko.mysql.binlog.event.AnnotateRowsEventData; +import com.github.shyiko.mysql.binlog.event.MariadbGtidEventData; +import com.github.shyiko.mysql.binlog.event.deserialization.EventDeserializer; +import org.testng.SkipException; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.concurrent.TimeUnit; + +import static org.testng.Assert.assertNotEquals; +import static org.testng.AssertJUnit.assertNotNull; + +/** + * @author Winger + */ +public class MariadbBinaryLogClientIntegrationTest extends AbstractIntegrationTest { + @Override + protected MysqlOnetimeServerOptions getOptions() { + MysqlOnetimeServerOptions options = super.getOptions(); + if ( !mysqlVersion.isMaria ) + return options; + + if ( options.extraParams == null ) + options.extraParams = ""; + else + options.extraParams += " "; + + options.extraParams += "--binlog-annotate-row-events"; + return options; + } + + @BeforeMethod + public void checkMariaDB() throws Exception { + if ( !mysqlVersion.isMaria ) + throw new SkipException("not maria"); + } + + @Test + public void testMariadbUseGTIDAndAnnotateRowsEvent() throws Exception { + master.execute(new BinaryLogClientIntegrationTest.Callback() { + @Override + public void execute(Statement statement) throws SQLException { + statement.execute("drop database if exists mbcj_test"); + statement.execute("create database mbcj_test"); + statement.execute("use mbcj_test"); + statement.execute("CREATE TABLE if not exists foo (i int)"); + statement.execute("CREATE TABLE if not exists bar (i int)"); + } + }); + // get current gtid + final String[] currentGtidPos = new String[1]; + master.query("show global variables like 'gtid_current_pos%'", new BinaryLogClientIntegrationTest.Callback() { + + @Override + public void execute(ResultSet rs) throws SQLException { + rs.next(); + currentGtidPos[0] = rs.getString(2); + } + }); + + CountDownEventListener eventListener; + BinaryLogClient client = new BinaryLogClient(master.hostname(), master.port(), master.username(), master.password()); + client.setGtidSet(currentGtidPos[0]); + client.setUseSendAnnotateRowsEvent(true); + + EventDeserializer eventDeserializer = new EventDeserializer(); + eventDeserializer.setCompatibilityMode(EventDeserializer.CompatibilityMode.CHAR_AND_BINARY_AS_BYTE_ARRAY, + EventDeserializer.CompatibilityMode.DATE_AND_TIME_AS_LONG); + client.setEventDeserializer(eventDeserializer); + client.registerEventListener(new TraceEventListener()); + client.registerLifecycleListener(new TraceLifecycleListener()); + client.registerEventListener(eventListener = new CountDownEventListener()); + + master.execute(new BinaryLogClientIntegrationTest.Callback() { + @Override + public void execute(Statement statement) throws SQLException { + statement.execute("INSERT INTO foo set i = 2"); + statement.execute("DROP TABLE IF EXISTS bar"); + } + }); + + try { + eventListener.reset(); + client.connect(5000); + + eventListener.waitFor(MariadbGtidEventData.class, 1, TimeUnit.SECONDS.toMillis(4)); + String gtidSet = client.getGtidSet(); + assertNotNull(gtidSet); + + eventListener.waitFor(AnnotateRowsEventData.class, 1, TimeUnit.SECONDS.toMillis(4)); + gtidSet = client.getGtidSet(); + assertNotEquals(currentGtidPos[0], gtidSet); + } finally { + client.disconnect(); + } + } +} diff --git a/src/test/java/com/github/shyiko/mysql/binlog/MariadbGtidSetTest.java b/src/test/java/com/github/shyiko/mysql/binlog/MariadbGtidSetTest.java new file mode 100644 index 00000000..0c282405 --- /dev/null +++ b/src/test/java/com/github/shyiko/mysql/binlog/MariadbGtidSetTest.java @@ -0,0 +1,46 @@ +package com.github.shyiko.mysql.binlog; + +import org.testng.annotations.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; + +/** + * @author Winger + */ +public class MariadbGtidSetTest { + + @Test + public void testAdd() { + MariadbGtidSet gtidSet = new MariadbGtidSet("0-102-7255"); + gtidSet.add("0-102-7256"); + gtidSet.add("0-102-7257"); + gtidSet.add("0-102-7259"); + gtidSet.add("1-102-7300"); + assertNotEquals(gtidSet.toString(), "1-102-7300"); + assertNotEquals(gtidSet.toString(), "0-102-7259"); + assertEquals(gtidSet.toString(), "0-102-7259,1-102-7300"); + } + + @Test + public void testEmptySet() { + assertEquals(new MariadbGtidSet("").toString(), ""); + } + + @Test + public void testEquals() { + assertEquals(new MariadbGtidSet(""), new MariadbGtidSet(null)); + assertEquals(new MariadbGtidSet(""), new MariadbGtidSet("")); + assertEquals(new MariadbGtidSet("0-0-7404"), new MariadbGtidSet("0-0-7404")); + } + + @Test + public void testMatcher() { + assertTrue(MariadbGtidSet.isMariaGtidSet("0-0-3323")); + assertTrue(MariadbGtidSet.isMariaGtidSet("0-0-3323,4-33-12342134,444-33-13412341233")); + assertTrue(MariadbGtidSet.isMariaGtidSet("0-0-3323, 4-33-12342134, 444-33-13412341233")); + assertFalse(MariadbGtidSet.isMariaGtidSet("07212070-4330-3bc8-8a3a-01e34be47bc3:1-141692942,a0c4a949-fae8-30f3-a4d2-fee56a1a9307:1-1427643460,a16ef643-1d4a-3fd9-a86e-1adeb836eb2d:1-1411988930,b0d822f4-5a84-30d3-a929-61f64740d7ac:1-59364")); + } +} diff --git a/src/test/java/com/github/shyiko/mysql/binlog/MySQLConnection.java b/src/test/java/com/github/shyiko/mysql/binlog/MySQLConnection.java new file mode 100644 index 00000000..3ba13a8e --- /dev/null +++ b/src/test/java/com/github/shyiko/mysql/binlog/MySQLConnection.java @@ -0,0 +1,117 @@ +package com.github.shyiko.mysql.binlog; + +import java.io.Closeable; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * Representation of a MySQL connection. + */ +public final class MySQLConnection implements Closeable { + + public final String hostname; + public final int port; + public final String username; + public final String password; + public Connection connection; + + public MySQLConnection(String hostname, int port, String username, String password) + throws ClassNotFoundException, SQLException { + this.hostname = hostname; + this.port = port; + this.username = username; + this.password = password; + Class.forName("com.mysql.jdbc.Driver"); + connect(); + } + + private void connect() throws SQLException { + this.connection = DriverManager.getConnection("jdbc:mysql://" + hostname + ":" + port + + "?serverTimezone=UTC", username, password); + execute(new BinaryLogClientIntegrationTest.Callback() { + + @Override + public void execute(Statement statement) throws SQLException { + statement.execute("SET time_zone = '+00:00'"); + } + }); + } + + public String hostname() { + return hostname; + } + + public int port() { + return port; + } + + public String username() { + return username; + } + + public String password() { + return password; + } + + public void execute(BinaryLogClientIntegrationTest.Callback callback, boolean autocommit) throws SQLException { + connection.setAutoCommit(autocommit); + Statement statement = connection.createStatement(); + try { + callback.execute(statement); + if ( !autocommit ) { + connection.commit(); + } + } finally { + statement.close(); + } + } + + public void execute(BinaryLogClientIntegrationTest.Callback callback) throws SQLException { + execute(callback, false); + } + + public void execute(final String... statements) throws SQLException { + execute(new BinaryLogClientIntegrationTest.Callback() { + @Override + public void execute(Statement statement) throws SQLException { + for ( String command : statements ) { + statement.execute(command); + } + } + }); + } + + public void query(String sql, BinaryLogClientIntegrationTest.Callback callback) throws SQLException { + connection.setAutoCommit(false); + Statement statement = connection.createStatement(); + try { + ResultSet rs = statement.executeQuery(sql); + try { + callback.execute(rs); + connection.commit(); + } finally { + rs.close(); + } + } finally { + statement.close(); + } + } + + @Override + public void close() throws IOException { + try { + connection.close(); + } catch ( SQLException e ) { + throw new IOException(e); + } + } + + public void reconnect() throws IOException, SQLException { + close(); + connect(); + } +} diff --git a/src/test/java/com/github/shyiko/mysql/binlog/MysqlOnetimeServer.java b/src/test/java/com/github/shyiko/mysql/binlog/MysqlOnetimeServer.java new file mode 100644 index 00000000..12d3cd90 --- /dev/null +++ b/src/test/java/com/github/shyiko/mysql/binlog/MysqlOnetimeServer.java @@ -0,0 +1,284 @@ +package com.github.shyiko.mysql.binlog; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.sql.*; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import java.util.logging.Logger; +import java.util.logging.Level; + +public class MysqlOnetimeServer { + private final MysqlOnetimeServerOptions options; + public static int nextServerID = 1; + public final int SERVER_ID = MysqlOnetimeServer.nextServerID++; + private final Logger logger = Logger.getLogger(getClass().getName()); + + + private Connection connection; + private int port; + private int serverPid; + public String path; + + public static final TypeReference> MAP_STRING_OBJECT_REF = new TypeReference>() {}; + + public MysqlOnetimeServer() { + this.options = new MysqlOnetimeServerOptions(); + } + + public MysqlOnetimeServer(MysqlOnetimeServerOptions options) { + this.options = options == null ? new MysqlOnetimeServerOptions() : options; + } + + public void boot() throws Exception { + final String dir = System.getProperty("user.dir"); + final String xtraParams = options.extraParams == null ? "" : options.extraParams; + + // By default, MySQL doesn't run under root. However, in an environment like Docker, the root user is the + // only available user by default. By adding "--user=root" when the root user is used, we can make sure + // the tests can continue to run. + boolean isRoot = System.getProperty("user.name").equals("root"); + + String gtidParams = ""; + if ( options.gtid ) { + logger.info("In gtid test mode."); + gtidParams = + "--gtid-mode=ON " + + "--log-slave-updates=ON " + + "--enforce-gtid-consistency=true "; + } + String serverID = ""; + if ( !xtraParams.contains("--server_id") ) + serverID = "--server_id=" + options.serverID; + + String authPlugin = ""; + + if ( getVersion().atLeast(8, 0) && !xtraParams.contains("--default-authentication-plugin")) { + authPlugin = "--default-authentication-plugin=mysql_native_password"; + } + + String fullRowMetaData = ""; + if ( getVersion().atLeast(8, 0) && options.fullRowMetaData ) { + fullRowMetaData = "--binlog-row-metadata=FULL"; + } + + ProcessBuilder pb = new ProcessBuilder( + dir + "/src/test/onetimeserver", + "--debug", + "--mysql-version=" + getVersionString(), + "--log-slave-updates", + "--log-bin=master", + "--binlog_format=row", + "--innodb_flush_log_at_trx_commit=0", + serverID, + "--character-set-server=utf8", + "--sync_binlog=0", + "--default-time-zone=+00:00", + fullRowMetaData, + isRoot ? "--user=root" : "", + authPlugin, + gtidParams + ); + + for ( String s : xtraParams.split(" ") ) { + pb.command().add(s); + } + + Process p = pb.start(); + BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); + + p.waitFor(); + + final BufferedReader errReader = new BufferedReader(new InputStreamReader(p.getErrorStream())); + + new Thread(() -> { + while (true) { + String l = null; + try { + l = errReader.readLine(); + } catch ( IOException e) {}; + + if (l == null) + break; + System.err.println(l); + } + }).start(); + + String json = reader.readLine(); + String outputFile; + try { + ObjectMapper mapper = new ObjectMapper(); + Map output = mapper.readValue(json, MAP_STRING_OBJECT_REF); + this.port = (Integer) output.get("port"); + this.serverPid = (Integer) output.get("server_pid"); + this.path = (String) output.get("mysql_path"); + outputFile = (String) output.get("output"); + } catch ( Exception e ) { + logger.log(Level.SEVERE, "got exception while parsing " + json, e); + throw(e); + } + + + resetConnection(); + this.connection.createStatement().executeUpdate("CREATE USER 'maxwell'@'127.0.0.1' IDENTIFIED BY 'maxwell'"); + this.connection.createStatement().executeUpdate("GRANT REPLICATION SLAVE on *.* to 'maxwell'@'127.0.0.1'"); + this.connection.createStatement().executeUpdate("GRANT ALL on *.* to 'maxwell'@'127.0.0.1'"); + this.connection.createStatement().executeUpdate("CREATE DATABASE if not exists test"); + logger.info("booted at port " + this.port + ", outputting to file " + outputFile); + + if ( options.masterServer != null ) { + this.setupSlave(options.masterServer.port); + } + } + + public void setupSlave(int masterPort) throws SQLException { + Connection master = DriverManager.getConnection("jdbc:mysql://127.0.0.1:" + masterPort + "/mysql?useSSL=false", "root", ""); + ResultSet rs = master.createStatement().executeQuery("show master status"); + if ( !rs.next() ) + throw new RuntimeException("could not get master status"); + + String file = rs.getString("File"); + Long position = rs.getLong("Position"); + rs.close(); + + String changeSQL = String.format( + "CHANGE MASTER to master_host = '127.0.0.1', master_user='maxwell', master_password='maxwell', " + + "master_log_file = '%s', master_log_pos = %d, master_port = %d", + file, position, masterPort + ); + logger.info("starting up slave: " + changeSQL); + getConnection().createStatement().execute(changeSQL); + getConnection().createStatement().execute("START SLAVE"); + + + rs.close(); + + ResultSet status = query("show slave status"); + if ( !status.next() ) + throw new RuntimeException("could not get slave status"); + + if ( status.getString("Slave_IO_Running").equals("No") + || status.getString("Slave_SQL_Running").equals("No")) { + throw new RuntimeException("could not start slave: " + dumpQuery("show slave status")); + + } + status.close(); + } + + + public String dumpQuery(String query) throws SQLException { + String result = ""; + ResultSet rs = getConnection().createStatement().executeQuery(query); + rs.next(); + for ( int i = 1 ; i <= rs.getMetaData().getColumnCount() ; i++) { + Object val = rs.getObject(i); + String asString = val == null ? "null" : val.toString(); + result = result + rs.getMetaData().getColumnName(i) + ": " + asString + "\n"; + } + return result; + } + + public void resetConnection() throws SQLException { + this.connection = getNewConnection(); + } + + public Connection getNewConnection() throws SQLException { + return DriverManager.getConnection("jdbc:mysql://127.0.0.1:" + port + "/mysql?zeroDateTimeBehavior=convertToNull&useSSL=false", "root", ""); + } + + public Connection getConnection() { + return connection; + } + + public Connection getConnection(String defaultDB) throws SQLException { + Connection conn = getNewConnection(); + conn.setCatalog(defaultDB); + return conn; + } + + public void execute(String query) throws SQLException { + Statement s = getConnection().createStatement(); + s.executeUpdate(query); + s.close(); + } + + private Connection cachedCX; + public void executeCached(String query) throws SQLException { + if ( cachedCX == null ) + cachedCX = getConnection(); + + Statement s = cachedCX.createStatement(); + s.executeUpdate(query); + s.close(); + } + + public void executeList(List queries) throws SQLException { + for (String q: queries) { + if ( q.matches("^\\s*$") ) + continue; + + execute(q); + } + } + + public void executeList(String[] schemaSQL) throws SQLException { + executeList(Arrays.asList(schemaSQL)); + } + + public void executeQuery(String sql) throws SQLException { + getConnection().createStatement().executeUpdate(sql); + } + + public ResultSet query(String sql) throws SQLException { + return getConnection().createStatement().executeQuery(sql); + } + + public int getPort() { + return port; + } + + public void shutDown() { + try { + Runtime.getRuntime().exec("kill " + this.serverPid); + } catch ( IOException e ) {} + } + + public static MysqlVersion getVersion() { + String version = getVersionString(); + if ( version.equals("mariadb") ) { + return new MysqlVersion(0, 0, true); + } else { + String[] parts = version.split("\\."); + return new MysqlVersion(Integer.valueOf(parts[0]), Integer.valueOf(parts[1]), false); + } + } + + private static String getVersionString() { + String mysqlVersion = System.getenv("MYSQL_VERSION"); + return mysqlVersion == null ? "5.7" : mysqlVersion; + } + + public void waitForSlaveToBeCurrent(MysqlOnetimeServer master) throws Exception { + ResultSet ms = master.query("show master status"); + ms.next(); + String masterFile = ms.getString("File"); + Long masterPos = ms.getLong("Position"); + ms.close(); + + while ( true ) { + ResultSet rs = query("show slave status"); + rs.next(); + if ( rs.getString("Relay_Master_Log_File").equals(masterFile) && + rs.getLong("Exec_Master_Log_Pos") >= masterPos ) + return; + + Thread.sleep(200); + } + } +} diff --git a/src/test/java/com/github/shyiko/mysql/binlog/MysqlOnetimeServerOptions.java b/src/test/java/com/github/shyiko/mysql/binlog/MysqlOnetimeServerOptions.java new file mode 100644 index 00000000..ae677cae --- /dev/null +++ b/src/test/java/com/github/shyiko/mysql/binlog/MysqlOnetimeServerOptions.java @@ -0,0 +1,9 @@ +package com.github.shyiko.mysql.binlog; + +public class MysqlOnetimeServerOptions { + public int serverID = MysqlOnetimeServer.nextServerID++; + public boolean gtid = false; + public MysqlOnetimeServer masterServer; + public String extraParams; + public boolean fullRowMetaData; +} diff --git a/src/test/java/com/github/shyiko/mysql/binlog/MysqlVersion.java b/src/test/java/com/github/shyiko/mysql/binlog/MysqlVersion.java new file mode 100644 index 00000000..949873b6 --- /dev/null +++ b/src/test/java/com/github/shyiko/mysql/binlog/MysqlVersion.java @@ -0,0 +1,47 @@ +package com.github.shyiko.mysql.binlog; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +public class MysqlVersion { + public boolean isMaria; + private final int major; + private final int minor; + + public MysqlVersion(int major, int minor, boolean isMaria) { + this.major = major; + this.minor = minor; + this.isMaria = isMaria; + } + + public boolean atLeast(int major, int minor) { + return (this.major > major) || (this.major == major && this.minor >= minor); + } + + public boolean atLeast(MysqlVersion version) { + return atLeast(version.major, version.minor); + } + + public boolean lessThan(int major, int minor) { + return (this.major < major) || (this.major == major & this.minor < minor); + } + + public static MysqlVersion capture(Connection c) throws SQLException { + DatabaseMetaData meta = c.getMetaData(); + return new MysqlVersion(meta.getDatabaseMajorVersion(), meta.getDatabaseMinorVersion(), false); + } + + public int getMajor() { + return this.major; + } + + public int getMinor() { + return this.minor; + } + + @Override + public String toString() { + return "MysqlVersion[" + this.major + "," + this.minor + "]"; + } +} diff --git a/src/test/java/com/github/shyiko/mysql/binlog/TCPReverseProxy.java b/src/test/java/com/github/shyiko/mysql/binlog/TCPReverseProxy.java index 883b3f25..0fe52e2c 100644 --- a/src/test/java/com/github/shyiko/mysql/binlog/TCPReverseProxy.java +++ b/src/test/java/com/github/shyiko/mysql/binlog/TCPReverseProxy.java @@ -36,7 +36,7 @@ */ public class TCPReverseProxy { - private final Logger logger = Logger.getLogger(getClass().getSimpleName()); + private final Logger logger = Logger.getLogger("donkey"); private final int port; private final String targetHost; diff --git a/src/test/java/com/github/shyiko/mysql/binlog/TraceEventListener.java b/src/test/java/com/github/shyiko/mysql/binlog/TraceEventListener.java index 0eacd643..7ec1ed18 100644 --- a/src/test/java/com/github/shyiko/mysql/binlog/TraceEventListener.java +++ b/src/test/java/com/github/shyiko/mysql/binlog/TraceEventListener.java @@ -25,7 +25,7 @@ */ public class TraceEventListener implements BinaryLogClient.EventListener { - private final Logger logger = Logger.getLogger(getClass().getSimpleName()); + private final Logger logger = Logger.getLogger("donkey"); @Override public void onEvent(Event event) { diff --git a/src/test/java/com/github/shyiko/mysql/binlog/TraceLifecycleListener.java b/src/test/java/com/github/shyiko/mysql/binlog/TraceLifecycleListener.java index 6e3f5702..f4fde7e2 100644 --- a/src/test/java/com/github/shyiko/mysql/binlog/TraceLifecycleListener.java +++ b/src/test/java/com/github/shyiko/mysql/binlog/TraceLifecycleListener.java @@ -23,7 +23,7 @@ */ public class TraceLifecycleListener implements BinaryLogClient.LifecycleListener { - private final Logger logger = Logger.getLogger(getClass().getSimpleName()); + private final Logger logger = Logger.getLogger("donkey"); @Override public void onConnect(BinaryLogClient client) { diff --git a/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/AnnotateRowsEventDataDeserializerTest.java b/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/AnnotateRowsEventDataDeserializerTest.java new file mode 100644 index 00000000..e117d32f --- /dev/null +++ b/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/AnnotateRowsEventDataDeserializerTest.java @@ -0,0 +1,27 @@ +package com.github.shyiko.mysql.binlog.event.deserialization; + +import com.github.shyiko.mysql.binlog.event.AnnotateRowsEventData; +import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; +import org.junit.Test; + +import java.io.IOException; + +import static junit.framework.Assert.assertEquals; + +/** + * @author Winger + */ +public class AnnotateRowsEventDataDeserializerTest { + + private static final byte[] DATA = {73, 78, 83, 69, 82, 84, 32, 73, 78, 84, 79, 32, 102, 111, 111, 32, 115, 101, 116, 32, 105, 32, 61, 32, 50}; + + private static final String sql = "INSERT INTO foo set i = 2"; + + @Test + public void deserialize() throws IOException { + AnnotateRowsEventDataDeserializer deserializer = new AnnotateRowsEventDataDeserializer(); + AnnotateRowsEventData eventData = deserializer.deserialize(new ByteArrayInputStream(DATA)); + + assertEquals(sql, eventData.getRowsQuery()); + } +} diff --git a/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/MariadbGtidEventDataDeserializerTest.java b/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/MariadbGtidEventDataDeserializerTest.java new file mode 100644 index 00000000..f6e703bf --- /dev/null +++ b/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/MariadbGtidEventDataDeserializerTest.java @@ -0,0 +1,26 @@ +package com.github.shyiko.mysql.binlog.event.deserialization; + +import com.github.shyiko.mysql.binlog.event.MariadbGtidEventData; +import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; +import org.junit.Test; + +import java.io.IOException; + +import static junit.framework.Assert.assertEquals; + +/** + * @author Winger + */ +public class MariadbGtidEventDataDeserializerTest { + + private static final byte[] DATA = {-20, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 121}; + + private static final String GTID_SET = "0-0-7404"; + + @Test + public void deserialize() throws IOException { + MariadbGtidEventDataDeserializer deserializer = new MariadbGtidEventDataDeserializer(); + MariadbGtidEventData eventData = deserializer.deserialize(new ByteArrayInputStream(DATA)); + assertEquals(GTID_SET, eventData.toString()); + } +} diff --git a/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/MariadbGtidListEventDataDeserializerTest.java b/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/MariadbGtidListEventDataDeserializerTest.java new file mode 100644 index 00000000..3a6f33fc --- /dev/null +++ b/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/MariadbGtidListEventDataDeserializerTest.java @@ -0,0 +1,26 @@ +package com.github.shyiko.mysql.binlog.event.deserialization; + +import com.github.shyiko.mysql.binlog.event.MariadbGtidListEventData; +import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; +import org.junit.Test; + +import java.io.IOException; + +import static junit.framework.Assert.assertEquals; + +/** + * @author Winger + */ +public class MariadbGtidListEventDataDeserializerTest { + + private static final byte[] DATA = {1, 0, 0, 0, 0, 0, 0, 0, 102, 0, 0, 0, 87, 28, 0, 0, 0, 0, 0, 0, 77}; + + private static final String GTID_SET_LIST = "MariadbGtidListEventData{mariaGTIDSet=0-102-7255}"; + + @Test + public void deserialize() throws IOException { + MariadbGtidListEventDataDeserializer deserializer = new MariadbGtidListEventDataDeserializer(); + MariadbGtidListEventData eventData = deserializer.deserialize(new ByteArrayInputStream(DATA)); + assertEquals(GTID_SET_LIST, eventData.toString()); + } +} diff --git a/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/TableMapEventMetadataDeserializerTest.java b/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/TableMapEventMetadataDeserializerTest.java new file mode 100644 index 00000000..1676293a --- /dev/null +++ b/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/TableMapEventMetadataDeserializerTest.java @@ -0,0 +1,43 @@ +package com.github.shyiko.mysql.binlog.event.deserialization; + +import com.github.shyiko.mysql.binlog.event.TableMapEventMetadata; +import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.testng.Assert.assertEquals; + +/** + * @author Harvey Yue + */ +public class TableMapEventMetadataDeserializerTest { + + /** + * https://github.com/mysql/mysql-server/blob/8.0/libbinlogevents/include/rows_event.h#L185 + * There are some optional metadata defined. They are listed in the table + * Table_table_map_event_optional_metadata. Optional metadata fields + * follow null_bits. Whether binlogging an optional metadata is decided by the + * server. The order is not defined, so they can be binlogged in any order. + * + * @throws IOException + */ + @Test + public void deserialize() throws IOException { + byte[] metadataIncludingUnknownFieldType = {1, 2, 0, -128, 2, 9, 83, 6, 63, 7, 63, 8, 63, 9, 63}; + TableMapEventMetadataDeserializer deserializer = new TableMapEventMetadataDeserializer(); + TableMapEventMetadata tableMapEventMetadata = + deserializer.deserialize(new ByteArrayInputStream(metadataIncludingUnknownFieldType), 23, 8); + + Map expectedCharsetCollations = new LinkedHashMap<>(); + expectedCharsetCollations.put(6, 63); + expectedCharsetCollations.put(7, 63); + expectedCharsetCollations.put(8, 63); + expectedCharsetCollations.put(9, 63); + + assertEquals(tableMapEventMetadata.getDefaultCharset().getDefaultCharsetCollation(), 83); + assertEquals(tableMapEventMetadata.getDefaultCharset().getCharsetCollations(), expectedCharsetCollations); + } +} diff --git a/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/TransactionPayloadEventDataDeserializerTest.java b/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/TransactionPayloadEventDataDeserializerTest.java new file mode 100644 index 00000000..a3b8e76e --- /dev/null +++ b/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/TransactionPayloadEventDataDeserializerTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2013 Stanley Shyiko + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.shyiko.mysql.binlog.event.deserialization; + +import com.github.shyiko.mysql.binlog.event.EventType; +import com.github.shyiko.mysql.binlog.event.TransactionPayloadEventData; +import com.github.shyiko.mysql.binlog.event.XAPrepareEventData; +import com.github.shyiko.mysql.binlog.io.ByteArrayInputStream; +import org.testng.annotations.Test; + +import java.io.IOException; + +import static org.testng.Assert.assertEquals; + +/** + * @author Somesh Malviya + */ +public class TransactionPayloadEventDataDeserializerTest { + + /* DATA is a binary representation of following: + TransactionPayloadEventData{compression_type=0, payload_size=451, uncompressed_size='960', payload: + Event{header=EventHeaderV4{timestamp=1646406641000, eventType=QUERY, serverId=223344, headerLength=19, dataLength=57, nextPosition=0, flags=8}, data=QueryEventData{threadId=12, executionTime=0, errorCode=0, database='', sql='BEGIN'}} + Event{header=EventHeaderV4{timestamp=1646406641000, eventType=TABLE_MAP, serverId=223344, headerLength=19, dataLength=63, nextPosition=0, flags=0}, data=TableMapEventData{tableId=84, database='demo', table='movies', columnTypes=3, 15, 3, 15, 15, 15, 15, 15, 15, 15, 15, columnMetadata=0, 1024, 0, 1024, 1024, 4096, 2048, 1024, 1024, 1024, 1024, columnNullability={}, eventMetadata=TableMapEventMetadata{signedness={}, defaultCharset=255, charsetCollations=null, columnCharsets=null, columnNames=null, setStrValues=null, enumStrValues=null, geometryTypes=null, simplePrimaryKeys=null, primaryKeysWithPrefix=null, enumAndSetDefaultCharset=null, enumAndSetColumnCharsets=null,visibility=null}}} + Event{header=EventHeaderV4{timestamp=1646406641000, eventType=EXT_UPDATE_ROWS, serverId=223344, headerLength=19, dataLength=756, nextPosition=0, flags=0}, data=UpdateRowsEventData{tableId=84, includedColumnsBeforeUpdate={0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, includedColumns={0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, rows=[ + {before=[1, Once Upon a Time in the West, 1968, Italy, Western, Claudia Cardinale|Charles Bronson|Henry Fonda|Gabriele Ferzetti|Frank Wolff|Al Mulock|Jason Robards|Woody Strode|Jack Elam|Lionel Stander|Paolo Stoppa|Keenan Wynn|Aldo Sambrell, Sergio Leone, Ennio Morricone, Sergio Leone|Sergio Donati|Dario Argento|Bernardo Bertolucci, Tonino Delli Colli, Paramount Pictures], after=[1, Once Upon a Time in the West, 1968, Italy, Western|Action, Claudia Cardinale|Charles Bronson|Henry Fonda|Gabriele Ferzetti|Frank Wolff|Al Mulock|Jason Robards|Woody Strode|Jack Elam|Lionel Stander|Paolo Stoppa|Keenan Wynn|Aldo Sambrell, Sergio Leone, Ennio Morricone, Sergio Leone|Sergio Donati|Dario Argento|Bernardo Bertolucci, Tonino Delli Colli, Paramount Pictures]} + ]}} + Event{header=EventHeaderV4{timestamp=1646406641000, eventType=XID, serverId=223344, headerLength=19, dataLength=8, nextPosition=0, flags=0}, data=XidEventData{xid=31}} + } + */ + private static final byte[] DATA = { + 2, 1, 0, 3, 3, -4, -64, 3, 1, 3, -4, -61, 1, 0, 40, -75, 47, -3, 0, 88, -68, 13, 0, -90, -34, + 97, 57, 96, 103, -108, 14, 32, 1, 32, 8, -126, 32, 120, 18, 103, 8, -126, -114, 45, -84, -15, + -9, -66, 68, 74, -118, -40, 82, 68, -110, 16, 13, -122, 26, 35, 98, 20, 123, 16, 7, -5, -10, 69, + -128, 37, 107, 91, -42, 50, -10, -116, -6, -79, 51, 11, 93, -14, 73, 10, 87, 0, 81, 0, 81, 0, + -1, -95, 63, -53, -78, 76, -31, -116, -56, -15, -88, -70, 26, 36, -55, -28, -13, 44, 66, -60, + 56, 4, -3, 113, -122, -58, 35, -112, 8, 18, 41, 28, -37, -42, -96, -83, -124, -73, -75, 84, -29, + -48, 41, 62, -15, -88, -70, 6, 72, -110, -55, 71, -63, -125, 3, -90, -14, 103, -111, 67, 1, -98, + -3, -15, 71, -125, -126, 88, -108, -16, -1, -104, 7, 79, -24, 6, -66, -16, -57, 53, -113, -86, + -117, 33, 73, 38, -97, -100, -68, 96, 125, -103, -40, 32, 92, 7, 111, 51, -71, 110, -37, -109, + -44, 33, 42, -59, -99, 73, -49, -29, 69, 16, -71, 49, -18, 87, 73, 108, -35, -45, -54, 18, -41, + 41, 55, -22, -87, 37, -75, 81, 29, 117, -106, 67, -32, -73, 16, 91, -50, 29, 30, -89, -16, -31, + 0, 126, 7, 4, -120, 45, 39, -73, -126, -55, 45, -41, 106, 20, -87, -55, 125, 49, -56, -99, 120, + -63, 11, 4, -116, 57, 100, -71, 87, -109, -35, 44, -34, 110, -66, -32, -36, 62, -55, -46, 77, + 54, -27, 40, -111, -39, -61, 73, 86, -34, 77, 16, -11, -70, 26, 110, -78, 93, -85, 68, 124, 75, + -79, -62, 77, -70, -27, 110, -102, 104, -87, -61, -28, -59, -92, 16, 113, -87, 126, 112, -109, + 30, -86, -101, 19, 49, -22, -87, -44, 19, -55, -115, 41, 68, -68, -104, -38, 117, 34, -46, 81, + 98, 69, -123, -21, -1, -1, -65, -31, 4, 30, 85, 23, -125, 36, -103, 124, -70, -63, -119, 18, 5, + 96, -5, 58, 112, 106, 18, 9, -71, -45, -106, 62, -107, 120, -92, 57, -41, -106, 108, -50, -19, + 37, -101, 27, 55, -59, 35, 109, -102, 58, -82, -31, -37, 74, 54, -11, -108, -33, 86, 98, 67, 94, + -117, 71, -55, 110, 79, 47, -79, 65, -27, -66, -60, 3, -53, 61, -75, -9, 58, 34, -69, 113, 18, + 0, 9, -123, 64, 53, 121, 75, 21, -68, 7, 33, -73, -30, -127, -103, 9, 17, 66, -49, 84, 65, 2, + 43, 16, -125, 0, 43, 55, 114, 109, 4, -50, -64, -62, -64, 99, 0, 28, -96, 53, -96, -13, 0, -68, + 1, 0, 0 + }; + + // Compression type for Zstd is 0 + private static final int COMPRESSION_TYPE = 0; + private static final int PAYLOAD_SIZE = 451; + private static final int UNCOMPRESSED_SIZE = 960; + private static final int NUMBER_OF_UNCOMPRESSED_EVENTS = 4; + private static final String UNCOMPRESSED_UPDATE_EVENT = + new StringBuilder() + .append( + "UpdateRowsEventData{tableId=84, includedColumnsBeforeUpdate={0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, includedColumns={0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, rows=[\n") + .append( + " {before=[1, Once Upon a Time in the West, 1968, Italy, Western, Claudia Cardinale|Charles Bronson|Henry Fonda|Gabriele Ferzetti|Frank Wolff|Al Mulock|Jason Robards|Woody Strode|Jack Elam|Lionel Stander|Paolo Stoppa|Keenan Wynn|Aldo Sambrell, Sergio Leone, Ennio Morricone, Sergio Leone|Sergio Donati|Dario Argento|Bernardo Bertolucci, Tonino Delli Colli, Paramount Pictures],") + .append( + " after=[1, Once Upon a Time in the West, 1968, Italy, Western|Action, Claudia Cardinale|Charles Bronson|Henry Fonda|Gabriele Ferzetti|Frank Wolff|Al Mulock|Jason Robards|Woody Strode|Jack Elam|Lionel Stander|Paolo Stoppa|Keenan Wynn|Aldo Sambrell, Sergio Leone, Ennio Morricone, Sergio Leone|Sergio Donati|Dario Argento|Bernardo Bertolucci, Tonino Delli Colli, Paramount Pictures]}\n") + .append("]}") + .toString(); + + @Test + public void deserialize() throws IOException { + TransactionPayloadEventDataDeserializer deserializer = new TransactionPayloadEventDataDeserializer(); + TransactionPayloadEventData transactionPayloadEventData = + deserializer.deserialize(new ByteArrayInputStream(DATA)); + assertEquals(COMPRESSION_TYPE, transactionPayloadEventData.getCompressionType()); + assertEquals(PAYLOAD_SIZE, transactionPayloadEventData.getPayloadSize()); + assertEquals(UNCOMPRESSED_SIZE, transactionPayloadEventData.getUncompressedSize()); + assertEquals(NUMBER_OF_UNCOMPRESSED_EVENTS, transactionPayloadEventData.getUncompressedEvents().size()); + assertEquals(EventType.QUERY, transactionPayloadEventData.getUncompressedEvents().get(0).getHeader().getEventType()); + assertEquals(EventType.TABLE_MAP, transactionPayloadEventData.getUncompressedEvents().get(1).getHeader().getEventType()); + assertEquals(EventType.EXT_UPDATE_ROWS, transactionPayloadEventData.getUncompressedEvents().get(2).getHeader().getEventType()); + assertEquals(EventType.XID, transactionPayloadEventData.getUncompressedEvents().get(3).getHeader().getEventType()); + assertEquals(UNCOMPRESSED_UPDATE_EVENT, transactionPayloadEventData.getUncompressedEvents().get(2).getData().toString()); + } +} diff --git a/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonBinaryValueIntegrationTest.java b/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonBinaryValueIntegrationTest.java index cdf314f7..d5589ad0 100644 --- a/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonBinaryValueIntegrationTest.java +++ b/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonBinaryValueIntegrationTest.java @@ -19,25 +19,29 @@ import com.github.shyiko.mysql.binlog.BinaryLogClientIntegrationTest; import com.github.shyiko.mysql.binlog.CapturingEventListener; import com.github.shyiko.mysql.binlog.CountDownEventListener; +import com.github.shyiko.mysql.binlog.MySQLConnection; +import com.github.shyiko.mysql.binlog.MysqlOnetimeServer; import com.github.shyiko.mysql.binlog.TraceEventListener; import com.github.shyiko.mysql.binlog.TraceLifecycleListener; -import com.github.shyiko.mysql.binlog.event.Event; import com.github.shyiko.mysql.binlog.event.EventData; import com.github.shyiko.mysql.binlog.event.EventType; import com.github.shyiko.mysql.binlog.event.QueryEventData; +import com.github.shyiko.mysql.binlog.event.UpdateRowsEventData; import com.github.shyiko.mysql.binlog.event.WriteRowsEventData; import org.skyscreamer.jsonassert.JSONAssert; +import org.testng.SkipException; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import java.io.IOException; import java.io.Serializable; +import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLSyntaxErrorException; import java.sql.Statement; import java.util.List; -import java.util.ResourceBundle; import java.util.TimeZone; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -52,7 +56,7 @@ */ public class JsonBinaryValueIntegrationTest { - private static final long DEFAULT_TIMEOUT = TimeUnit.SECONDS.toMillis(3); + private static final long DEFAULT_TIMEOUT = TimeUnit.SECONDS.toMillis(6); private final Logger logger = Logger.getLogger(getClass().getSimpleName()); @@ -62,18 +66,21 @@ public class JsonBinaryValueIntegrationTest { private final TimeZone timeZoneBeforeTheTest = TimeZone.getDefault(); - private BinaryLogClientIntegrationTest.MySQLConnection master; + private MySQLConnection master; private BinaryLogClient client; private CountDownEventListener eventListener; + private boolean isMaria = "mariadb".equals(System.getenv("MYSQL_VERSION")); + @BeforeClass public void setUp() throws Exception { TimeZone.setDefault(TimeZone.getTimeZone("GMT")); - ResourceBundle bundle = ResourceBundle.getBundle("jdbc"); - String prefix = "jdbc.mysql.replication."; - master = new BinaryLogClientIntegrationTest.MySQLConnection(bundle.getString(prefix + "master.hostname"), - Integer.parseInt(bundle.getString(prefix + "master.port")), - bundle.getString(prefix + "master.username"), bundle.getString(prefix + "master.password")); + + MysqlOnetimeServer masterServer = new MysqlOnetimeServer(); + masterServer.boot(); + + master = new MySQLConnection("127.0.0.1", masterServer.getPort(), "root", ""); + client = new BinaryLogClient(master.hostname(), master.port(), master.username(), master.password()); client.setServerId(client.getServerId() - 1); // avoid clashes between BinaryLogClient instances client.setKeepAlive(false); @@ -98,26 +105,140 @@ public void execute(Statement statement) throws SQLException { }); } catch (SQLSyntaxErrorException e) { // Skip the tests altogether since MySQL is pre 5.7 + System.err.println("skipping JSON tests (pre 5.7)"); throw new org.testng.SkipException("JSON data type is not supported by current version of MySQL"); } - eventListener.waitFor(EventType.QUERY, 3, DEFAULT_TIMEOUT); + eventListener.waitForAtLeast(EventType.QUERY, 3, DEFAULT_TIMEOUT); eventListener.reset(); } + private String parseAndRemoveSpaces(byte[] jsonBinary) throws IOException { + String parsed = JsonBinary.parseAsString(jsonBinary); + return parsed.replaceAll(" ", ""); + } + + @Test + public void testMysql8JsonSetPartialUpdateWithHoles() throws Exception { + CapturingEventListener capturingEventListener = new CapturingEventListener(); + client.registerEventListener(capturingEventListener); + String json = "{\"age\":22,\"addr\":{\"code\":100,\"detail\":{\"ab\":\"970785C8-C299\"}},\"name\":\"Alice\"}"; + master.execute("DROP TABLE IF EXISTS json_test", "create table json_test (j JSON)", + "INSERT INTO json_test VALUES ('" + json + "')", + "UPDATE json_test SET j = JSON_SET(j, '$.addr.detail.ab', '970785C8')"); + capturingEventListener.waitFor(WriteRowsEventData.class, 1, DEFAULT_TIMEOUT); + capturingEventListener.waitFor(UpdateRowsEventData.class, 1, DEFAULT_TIMEOUT); + List events = capturingEventListener.getEvents(WriteRowsEventData.class); + Serializable[] insertData = events.iterator().next().getRows().get(0); + assertEquals(JsonBinary.parseAsString((byte[]) insertData[0]), json); + + List updateEvents = capturingEventListener.getEvents(UpdateRowsEventData.class); + Serializable[] updateData = updateEvents.iterator().next().getRows().get(0).getValue(); + assertEquals(parseAndRemoveSpaces((byte[]) updateData[0]), json.replace("970785C8-C299", "970785C8")); + } + + @Test + public void testMysql8JsonRemovePartialUpdateWithHoles() throws Exception { + CapturingEventListener capturingEventListener = new CapturingEventListener(); + client.registerEventListener(capturingEventListener); + String json = "{\"age\":22,\"addr\":{\"code\":100,\"detail\":{\"ab\":\"970785C8-C299\"}},\"name\":\"Alice\"}"; + master.execute("DROP TABLE IF EXISTS json_test", "create table json_test (j JSON)", + "INSERT INTO json_test VALUES ('" + json + "')", + "UPDATE json_test SET j = JSON_REMOVE(j, '$.addr.detail.ab')"); + capturingEventListener.waitFor(WriteRowsEventData.class, 1, DEFAULT_TIMEOUT); + capturingEventListener.waitFor(UpdateRowsEventData.class, 1, DEFAULT_TIMEOUT); + List events = capturingEventListener.getEvents(WriteRowsEventData.class); + Serializable[] insertData = events.iterator().next().getRows().get(0); + assertEquals(JsonBinary.parseAsString((byte[]) insertData[0]), json); + + List updateEvents = capturingEventListener.getEvents(UpdateRowsEventData.class); + Serializable[] updateData = updateEvents.iterator().next().getRows().get(0).getValue(); + assertEquals(parseAndRemoveSpaces((byte[]) updateData[0]), json.replace("\"ab\":\"970785C8-C299\"", "")); + + client.unregisterEventListener(capturingEventListener); + } + + @Test + public void testMysql8JsonRemovePartialUpdateWithHolesAndSparseKeys() throws Exception { + CapturingEventListener capturingEventListener = new CapturingEventListener(); + client.registerEventListener(capturingEventListener); + String json = "{\"17fc9889474028063990914001f6854f6b8b5784\":\"test_field_for_remove_fields_behaviour_2\",\"1f3a2ea5bc1f60258df20521bee9ac636df69a3a\":{\"currency\":\"USD\"},\"4f4d99a438f334d7dbf83a1816015b361b848b3b\":{\"currency\":\"USD\"},\"9021162291be72f5a8025480f44bf44d5d81d07c\":\"test_field_for_remove_fields_behaviour_3_will_be_removed\",\"9b0ed11532efea688fdf12b28f142b9eb08a80c5\":{\"currency\":\"USD\"},\"e65ad0762c259b05b4866f7249eabecabadbe577\":\"test_field_for_remove_fields_behaviour_1_updated\",\"ff2c07edcaa3e987c23fb5cc4fe860bb52becf00\":{\"currency\":\"USD\"}}"; + master.execute("DROP TABLE IF EXISTS json_test", "create table json_test (j JSON)", + "INSERT INTO json_test VALUES ('" + json + "')", + "UPDATE json_test SET j = JSON_REMOVE(j, '$.\"17fc9889474028063990914001f6854f6b8b5784\"')"); + capturingEventListener.waitFor(WriteRowsEventData.class, 1, DEFAULT_TIMEOUT); + capturingEventListener.waitFor(UpdateRowsEventData.class, 1, DEFAULT_TIMEOUT); + List events = capturingEventListener.getEvents(WriteRowsEventData.class); + Serializable[] insertData = events.iterator().next().getRows().get(0); + assertEquals(JsonBinary.parseAsString((byte[]) insertData[0]), json); + + List updateEvents = capturingEventListener.getEvents(UpdateRowsEventData.class); + Serializable[] updateData = updateEvents.iterator().next().getRows().get(0).getValue(); + assertEquals(parseAndRemoveSpaces((byte[]) updateData[0]), json.replace( + "\"17fc9889474028063990914001f6854f6b8b5784\":\"test_field_for_remove_fields_behaviour_2\",", "")); + + client.unregisterEventListener(capturingEventListener); + } + + @Test + public void testMysql8JsonReplacePartialUpdateWithHoles() throws Exception { + CapturingEventListener capturingEventListener = new CapturingEventListener(); + client.registerEventListener(capturingEventListener); + String json = "{\"age\":22,\"addr\":{\"code\":100,\"detail\":{\"ab\":\"970785C8-C299\"}},\"name\":\"Alice\"}"; + master.execute("DROP TABLE IF EXISTS json_test", "create table json_test (j JSON)", + "INSERT INTO json_test VALUES ('" + json + "')", + "UPDATE json_test SET j = JSON_REPLACE(j, '$.addr.detail.ab', '9707')"); + capturingEventListener.waitFor(WriteRowsEventData.class, 1, DEFAULT_TIMEOUT); + capturingEventListener.waitFor(UpdateRowsEventData.class, 1, DEFAULT_TIMEOUT); + List events = capturingEventListener.getEvents(WriteRowsEventData.class); + Serializable[] insertData = events.iterator().next().getRows().get(0); + assertEquals(JsonBinary.parseAsString((byte[]) insertData[0]), json); + + List updateEvents = capturingEventListener.getEvents(UpdateRowsEventData.class); + Serializable[] updateData = updateEvents.iterator().next().getRows().get(0).getValue(); + assertEquals(parseAndRemoveSpaces((byte[]) updateData[0]), json.replace("970785C8-C299", "9707")); + + client.unregisterEventListener(capturingEventListener); + } + + @Test + public void testMysql8JsonRemoveArrayValue() throws Exception { + CapturingEventListener capturingEventListener = new CapturingEventListener(); + client.registerEventListener(capturingEventListener); + + String json = "[\"foo\",\"bar\",\"baz\"]"; + master.execute("DROP TABLE IF EXISTS json_test", "create table json_test (j JSON)", + "INSERT INTO json_test VALUES ('" + json + "')", + "UPDATE json_test SET j = JSON_REMOVE(j, '$[1]')"); + capturingEventListener.waitFor(WriteRowsEventData.class, 1, DEFAULT_TIMEOUT); + capturingEventListener.waitFor(UpdateRowsEventData.class, 1, DEFAULT_TIMEOUT); + + List events = capturingEventListener.getEvents(WriteRowsEventData.class); + Serializable[] insertData = events.iterator().next().getRows().get(0); + assertEquals(JsonBinary.parseAsString((byte[]) insertData[0]), json); + + List updateEvents = capturingEventListener.getEvents(UpdateRowsEventData.class); + Serializable[] updateData = updateEvents.iterator().next().getRows().get(0).getValue(); + String parsed = parseAndRemoveSpaces((byte[]) updateData[0]); + + assertEquals(parsed, "[\"foo\",\"baz\"]"); + + client.unregisterEventListener(capturingEventListener); + } + @Test public void testValueBoundariesAreHonored() throws Exception { - CountDownEventListener eventListener = new CountDownEventListener(); - client.registerEventListener(eventListener); CapturingEventListener capturingEventListener = new CapturingEventListener(); client.registerEventListener(capturingEventListener); master.execute("create table json_b (h varchar(255), j JSON, k varchar(255))", "INSERT INTO json_b VALUES ('sponge', '{}', 'bob');"); - eventListener.waitFor(WriteRowsEventData.class, 1, DEFAULT_TIMEOUT); + capturingEventListener.waitFor(WriteRowsEventData.class, 1, DEFAULT_TIMEOUT); List events = capturingEventListener.getEvents(WriteRowsEventData.class); Serializable[] data = events.iterator().next().getRows().get(0); assertEquals(data[0], "sponge"); assertEquals(JsonBinary.parseAsString((byte[]) data[1]), "{}"); assertEquals(data[2], "bob"); + + client.unregisterEventListener(capturingEventListener); } @Test @@ -341,12 +462,18 @@ public void testEmptyArray() throws Exception { @Test public void testScalarDateTime() throws Exception { + if ( isMaria ) + throw new SkipException(""); + assertEquals(writeAndCaptureJSON("CAST(CAST('2015-01-15 23:24:25' AS DATETIME) AS JSON)"), "\"2015-01-15 23:24:25\""); } @Test public void testScalarTime() throws Exception { + if ( isMaria ) + throw new SkipException(""); + assertEquals(writeAndCaptureJSON("CAST(CAST('23:24:25' AS TIME) AS JSON)"), "\"23:24:25\""); assertEquals(writeAndCaptureJSON("CAST(CAST('23:24:25.12' AS TIME(3)) AS JSON)"), @@ -357,12 +484,17 @@ public void testScalarTime() throws Exception { @Test public void testScalarDate() throws Exception { + if ( isMaria ) + throw new SkipException(""); assertEquals(writeAndCaptureJSON("CAST(CAST('2015-01-15' AS DATE) AS JSON)"), "\"2015-01-15\""); } @Test public void testScalarTimestamp() throws Exception { + if ( isMaria ) + throw new SkipException(""); + // timestamp literals are interpreted by MySQL as DATETIME values assertEquals(writeAndCaptureJSON("CAST(TIMESTAMP'2015-01-15 23:24:25' AS JSON)"), "\"2015-01-15 23:24:25\""); @@ -377,6 +509,9 @@ public void testScalarTimestamp() throws Exception { @Test public void testScalarGeometry() throws Exception { + if ( isMaria ) + throw new SkipException(""); + assertEquals(writeAndCaptureJSON("CAST(ST_GeomFromText('POINT(1 1)') AS JSON)"), "{\"type\":\"Point\",\"coordinates\":[1.0,1.0]}"); } @@ -388,10 +523,26 @@ public void testScalarStringWithCharsetConversion() throws Exception { @Test public void testScalarBinaryAsBase64() throws Exception { + if ( isMaria ) + throw new SkipException(""); + assertEquals(writeAndCaptureJSON("CAST(x'cafe' AS JSON)"), "\"yv4=\""); assertEquals(writeAndCaptureJSON("CAST(x'cafebabe' AS JSON)"), "\"yv66vg==\""); } + @Test + public void testJsonNull() throws Exception { + master.execute(new BinaryLogClientIntegrationTest.Callback() { + @Override + public void execute(Statement statement) throws SQLException { + ResultSet results = statement.executeQuery("SELECT version()"); + results.next(); + System.out.println("MySQL version = " + results.getString(1)); + } + }); + assertJSONMatchOriginal("null"); + } + private void assertJSONMatchOriginal(String value) throws Exception { assetJSONEquals(value, writeAndCaptureJSON("'" + value + "'")); } @@ -408,32 +559,37 @@ private String writeAndCaptureJSON(final String value) throws Exception { CapturingEventListener capturingEventListener = new CapturingEventListener(); client.registerEventListener(capturingEventListener); try { - master.execute(new BinaryLogClientIntegrationTest.Callback() { - @Override - public void execute(Statement statement) throws SQLException { - statement.execute("drop table if exists data_type_hell"); - statement.execute("create table data_type_hell (column_ " + "JSON" + ")"); - statement.execute("insert into data_type_hell values (" + value + ")"); - } + master.execute(statement -> { + statement.execute("drop table if exists data_type_hell"); + statement.execute("create table data_type_hell (column_ " + "JSON" + ")"); + statement.execute("insert into data_type_hell values (" + value + ")"); }); - eventListener.waitFor(WriteRowsEventData.class, 1, DEFAULT_TIMEOUT); + capturingEventListener.waitFor(WriteRowsEventData.class, 1, DEFAULT_TIMEOUT); } finally { client.unregisterEventListener(capturingEventListener); } - byte[] b = (byte[]) capturingEventListener.getEvents(WriteRowsEventData.class).get(0).getRows().get(0)[0]; + if ( capturingEventListener.getEvents(WriteRowsEventData.class).size() == 0 ) { + System.out.println("I am about to fail an expectation..."); + assertTrue(false, "did not receive rows in json test for " + value); + } + WriteRowsEventData e = capturingEventListener.getEvents(WriteRowsEventData.class).get(0); + Serializable[] firstRow = e.getRows().get(0); + + byte[] b = (byte[]) firstRow[0]; return b == null ? null : JsonBinary.parseAsString(b); } + @AfterMethod public void afterEachTest() throws Exception { final CountDownLatch latch = new CountDownLatch(1); final String markerQuery = "drop table if exists _EOS_marker"; - BinaryLogClient.EventListener markerInterceptor = new BinaryLogClient.EventListener() { - @Override - public void onEvent(Event event) { - if (event.getHeader().getEventType() == EventType.QUERY) { - EventData data = event.getData(); - if (data != null && ((QueryEventData) data).getSql().contains("_EOS_marker")) { + BinaryLogClient.EventListener markerInterceptor = event -> { + if (event.getHeader().getEventType() == EventType.QUERY) { + EventData data = event.getData(); + if (data != null) { + String sql = ((QueryEventData) data).getSql().toLowerCase(); + if (sql.contains("_EOS_marker".toLowerCase())) { latch.countDown(); } } diff --git a/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonPartialUpdateParseTest.java b/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonPartialUpdateParseTest.java new file mode 100644 index 00000000..8d347dba --- /dev/null +++ b/src/test/java/com/github/shyiko/mysql/binlog/event/deserialization/json/JsonPartialUpdateParseTest.java @@ -0,0 +1,109 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.shyiko.mysql.binlog.event.deserialization.json; + +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Test; + +/** + * The column value + *

+ * {
+ *    "17fc9889474028063990914001f6854f6b8b5784":"test_field_for_remove_fields_behaviour_2",
+ *    "1f3a2ea5bc1f60258df20521bee9ac636df69a3a":{
+ *       "currency":"USD"
+ *    },
+ *    "4f4d99a438f334d7dbf83a1816015b361b848b3b":{
+ *       "currency":"USD"
+ *    },
+ *    "9021162291be72f5a8025480f44bf44d5d81d07c":"test_field_for_remove_fields_behaviour_3_will_be_removed",
+ *    "9b0ed11532efea688fdf12b28f142b9eb08a80c5":{
+ *       "currency":"USD"
+ *    },
+ *    "e65ad0762c259b05b4866f7249eabecabadbe577":"test_field_for_remove_fields_behaviour_1_updated",
+ *    "ff2c07edcaa3e987c23fb5cc4fe860bb52becf00":{
+ *       "currency":"USD"
+ *    }
+ * }
+ * 
+ * is partially updated using + *
+ * JSON_REMOVE(custom_fields, '$.\"17fc9889474028063990914001f6854f6b8b5784\"')
+ * 
+ * MySQL 5.7 and MySQL 8.0 emits different values in binlog (MySQL 8 value is sparse and requires strict using of offsets) + */ +public class JsonPartialUpdateParseTest { + + private static final byte[] BINLOG_UPDATE_57 = { 0, 6, 0, -28, 1, 46, 0, 40, 0, 86, 0, 40, 0, 126, 0, 40, 0, -90, 0, + 40, 0, -50, 0, 40, 0, -10, 0, 40, 0, 0, 30, 1, 0, 53, 1, 12, 76, 1, 0, -123, 1, 12, -100, 1, 0, -51, 1, 49, + 102, 51, 97, 50, 101, 97, 53, 98, 99, 49, 102, 54, 48, 50, 53, 56, 100, 102, 50, 48, 53, 50, 49, 98, 101, + 101, 57, 97, 99, 54, 51, 54, 100, 102, 54, 57, 97, 51, 97, 52, 102, 52, 100, 57, 57, 97, 52, 51, 56, 102, + 51, 51, 52, 100, 55, 100, 98, 102, 56, 51, 97, 49, 56, 49, 54, 48, 49, 53, 98, 51, 54, 49, 98, 56, 52, 56, + 98, 51, 98, 57, 48, 50, 49, 49, 54, 50, 50, 57, 49, 98, 101, 55, 50, 102, 53, 97, 56, 48, 50, 53, 52, 56, + 48, 102, 52, 52, 98, 102, 52, 52, 100, 53, 100, 56, 49, 100, 48, 55, 99, 57, 98, 48, 101, 100, 49, 49, 53, + 51, 50, 101, 102, 101, 97, 54, 56, 56, 102, 100, 102, 49, 50, 98, 50, 56, 102, 49, 52, 50, 98, 57, 101, 98, + 48, 56, 97, 56, 48, 99, 53, 101, 54, 53, 97, 100, 48, 55, 54, 50, 99, 50, 53, 57, 98, 48, 53, 98, 52, 56, + 54, 54, 102, 55, 50, 52, 57, 101, 97, 98, 101, 99, 97, 98, 97, 100, 98, 101, 53, 55, 55, 102, 102, 50, 99, + 48, 55, 101, 100, 99, 97, 97, 51, 101, 57, 56, 55, 99, 50, 51, 102, 98, 53, 99, 99, 52, 102, 101, 56, 54, + 48, 98, 98, 53, 50, 98, 101, 99, 102, 48, 48, 1, 0, 23, 0, 11, 0, 8, 0, 12, 19, 0, 99, 117, 114, 114, 101, + 110, 99, 121, 3, 85, 83, 68, 1, 0, 23, 0, 11, 0, 8, 0, 12, 19, 0, 99, 117, 114, 114, 101, 110, 99, 121, 3, + 85, 83, 68, 56, 116, 101, 115, 116, 95, 102, 105, 101, 108, 100, 95, 102, 111, 114, 95, 114, 101, 109, 111, + 118, 101, 95, 102, 105, 101, 108, 100, 115, 95, 98, 101, 104, 97, 118, 105, 111, 117, 114, 95, 51, 95, 119, + 105, 108, 108, 95, 98, 101, 95, 114, 101, 109, 111, 118, 101, 100, 1, 0, 23, 0, 11, 0, 8, 0, 12, 19, 0, 99, + 117, 114, 114, 101, 110, 99, 121, 3, 85, 83, 68, 48, 116, 101, 115, 116, 95, 102, 105, 101, 108, 100, 95, + 102, 111, 114, 95, 114, 101, 109, 111, 118, 101, 95, 102, 105, 101, 108, 100, 115, 95, 98, 101, 104, 97, + 118, 105, 111, 117, 114, 95, 49, 95, 117, 112, 100, 97, 116, 101, 100, 1, 0, 23, 0, 11, 0, 8, 0, 12, 19, 0, + 99, 117, 114, 114, 101, 110, 99, 121, 3, 85, 83, 68 }; + + private static final byte[] BINLOG_UPDATE_80 = { 0, 6, 0, 60, 2, 93, 0, 40, 0, -123, 0, 40, 0, -83, 0, 40, 0, -43, + 0, 40, 0, -3, 0, 40, 0, 37, 1, 40, 0, 0, 118, 1, 0, -115, 1, 12, -92, 1, 0, -35, 1, 12, -12, 1, 0, 37, 2, 1, + 12, -12, 1, 0, 37, 2, 49, 55, 102, 99, 57, 56, 56, 57, 52, 55, 52, 48, 50, 56, 48, 54, 51, 57, 57, 48, 57, + 49, 52, 48, 48, 49, 102, 54, 56, 53, 52, 102, 54, 98, 56, 98, 53, 55, 56, 52, 49, 102, 51, 97, 50, 101, 97, + 53, 98, 99, 49, 102, 54, 48, 50, 53, 56, 100, 102, 50, 48, 53, 50, 49, 98, 101, 101, 57, 97, 99, 54, 51, 54, + 100, 102, 54, 57, 97, 51, 97, 52, 102, 52, 100, 57, 57, 97, 52, 51, 56, 102, 51, 51, 52, 100, 55, 100, 98, + 102, 56, 51, 97, 49, 56, 49, 54, 48, 49, 53, 98, 51, 54, 49, 98, 56, 52, 56, 98, 51, 98, 57, 48, 50, 49, 49, + 54, 50, 50, 57, 49, 98, 101, 55, 50, 102, 53, 97, 56, 48, 50, 53, 52, 56, 48, 102, 52, 52, 98, 102, 52, 52, + 100, 53, 100, 56, 49, 100, 48, 55, 99, 57, 98, 48, 101, 100, 49, 49, 53, 51, 50, 101, 102, 101, 97, 54, 56, + 56, 102, 100, 102, 49, 50, 98, 50, 56, 102, 49, 52, 50, 98, 57, 101, 98, 48, 56, 97, 56, 48, 99, 53, 101, + 54, 53, 97, 100, 48, 55, 54, 50, 99, 50, 53, 57, 98, 48, 53, 98, 52, 56, 54, 54, 102, 55, 50, 52, 57, 101, + 97, 98, 101, 99, 97, 98, 97, 100, 98, 101, 53, 55, 55, 102, 102, 50, 99, 48, 55, 101, 100, 99, 97, 97, 51, + 101, 57, 56, 55, 99, 50, 51, 102, 98, 53, 99, 99, 52, 102, 101, 56, 54, 48, 98, 98, 53, 50, 98, 101, 99, + 102, 48, 48, 40, 116, 101, 115, 116, 95, 102, 105, 101, 108, 100, 95, 102, 111, 114, 95, 114, 101, 109, 111, + 118, 101, 95, 102, 105, 101, 108, 100, 115, 95, 98, 101, 104, 97, 118, 105, 111, 117, 114, 95, 50, 1, 0, 23, + 0, 11, 0, 8, 0, 12, 19, 0, 99, 117, 114, 114, 101, 110, 99, 121, 3, 85, 83, 68, 1, 0, 23, 0, 11, 0, 8, 0, + 12, 19, 0, 99, 117, 114, 114, 101, 110, 99, 121, 3, 85, 83, 68, 56, 116, 101, 115, 116, 95, 102, 105, 101, + 108, 100, 95, 102, 111, 114, 95, 114, 101, 109, 111, 118, 101, 95, 102, 105, 101, 108, 100, 115, 95, 98, + 101, 104, 97, 118, 105, 111, 117, 114, 95, 51, 95, 119, 105, 108, 108, 95, 98, 101, 95, 114, 101, 109, 111, + 118, 101, 100, 1, 0, 23, 0, 11, 0, 8, 0, 12, 19, 0, 99, 117, 114, 114, 101, 110, 99, 121, 3, 85, 83, 68, 48, + 116, 101, 115, 116, 95, 102, 105, 101, 108, 100, 95, 102, 111, 114, 95, 114, 101, 109, 111, 118, 101, 95, + 102, 105, 101, 108, 100, 115, 95, 98, 101, 104, 97, 118, 105, 111, 117, 114, 95, 49, 95, 117, 112, 100, 97, + 116, 101, 100, 1, 0, 23, 0, 11, 0, 8, 0, 12, 19, 0, 99, 117, 114, 114, 101, 110, 99, 121, 3, 85, 83, 68 }; + + @Test + public void test57PartialUpdateBinlog() throws IOException { + final String json = JsonBinary.parseAsString(BINLOG_UPDATE_57); + Assert.assertFalse(json.contains("17fc9889474028063990914001f6854f6b8b5784")); + Assert.assertTrue(json.contains("1f3a2ea5bc1f60258df20521bee9ac636df69a3a")); + } + + @Test + public void test80PartialUpdateBinlog() throws IOException { + final String json = JsonBinary.parseAsString(BINLOG_UPDATE_80); + Assert.assertFalse(json.contains("17fc9889474028063990914001f6854f6b8b5784")); + System.out.println(json); + Assert.assertTrue(json.contains("1f3a2ea5bc1f60258df20521bee9ac636df69a3a")); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/shyiko/mysql/binlog/io/ByteArrayInputStreamTest.java b/src/test/java/com/github/shyiko/mysql/binlog/io/ByteArrayInputStreamTest.java new file mode 100644 index 00000000..8f0eb112 --- /dev/null +++ b/src/test/java/com/github/shyiko/mysql/binlog/io/ByteArrayInputStreamTest.java @@ -0,0 +1,102 @@ +package com.github.shyiko.mysql.binlog.io; + +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +public class ByteArrayInputStreamTest { + @Test + public void testReadToArray() throws Exception { + byte[] buff = new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + ByteArrayInputStream in = new ByteArrayInputStream(buff); + assertEquals(in.getPosition(), 0); + + byte[] b = new byte[20]; + + int read = in.read(b, 0, 0); + assertEquals(read, 0); + assertEquals(in.getPosition(), 0); + + read = in.read(b, 0, 4); + assertEquals(read, 4); + assertEquals(b[3], 3); + assertEquals(in.getPosition(), 4); + + read = in.read(b, 4, 4); + assertEquals(read, 4); + assertEquals(b[7], 7); + assertEquals(in.getPosition(), 8); + + read = in.read(b, 8, 4); + assertEquals(read, 4); + assertEquals(b[11], 11); + assertEquals(in.getPosition(), 12); + + read = in.read(b, 12, 4); + assertEquals(read, 4); + assertEquals(b[15], 15); + assertEquals(in.getPosition(), 16); + + read = in.read(b, 16, 4); + assertEquals(read, -1); + assertEquals(in.getPosition(), 16); + } + + @Test + public void testReadToArrayWithinBlockBoundaries() throws Exception { + byte[] buff = new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8}; + ByteArrayInputStream in = new ByteArrayInputStream(buff); + byte[] b = new byte[8]; + + in.enterBlock(4); + + int read = in.read(b, 0, 3); + assertEquals(read, 3); + assertEquals(b[2], 2); + + read = in.read(b, 3, 3); + assertEquals(read, 1); + assertEquals(b[3], 3); + + read = in.read(b, 4, 3); + assertEquals(read, -1); + } + + @Test(expectedExceptions = NullPointerException.class) + public void testReadToArrayWithNullBuff() throws Exception { + ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{}); + in.read(null, 0, 4); + } + + @Test(expectedExceptions = IndexOutOfBoundsException.class) + public void testReadToArrayWhenLenExceedsBuffSize() throws Exception { + ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{0, 1, 2}); + byte[] b = new byte[1]; + in.read(b, 0, 4); + } + + @Test(expectedExceptions = IndexOutOfBoundsException.class) + public void testReadToArrayWhenOffsetNegative() throws Exception { + ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{0, 1, 2}); + byte[] b = new byte[1]; + in.read(b, -1, 1); + } + + @Test(expectedExceptions = IndexOutOfBoundsException.class) + public void testReadToArrayWhenLengthNegative() throws Exception { + ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{0, 1, 2}); + byte[] b = new byte[1]; + in.read(b, 0, -1); + } + + @Test + public void testPeekAndReadToArray() throws Exception { + ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{5, 6, 7}); + byte[] b = new byte[3]; + assertEquals(in.peek(), 5); + int read = in.read(b, 0, 3); + assertEquals(read, 3); + assertEquals(b[0], 5); + assertEquals(b[2], 7); + } +} diff --git a/src/test/onetimeserver b/src/test/onetimeserver new file mode 100755 index 00000000..a240a9c9 --- /dev/null +++ b/src/test/onetimeserver @@ -0,0 +1,117 @@ +#!/bin/bash + +_PLATFORM=`uname -sm` +PLATFORM=${_PLATFORM/ /-} + +OS=`uname -s | tr '[:upper:]' '[:lower:]'` + +THIS_URL="https://raw.githubusercontent.com/osheroff/onetimeserver/master/onetimeserver" +WRAPPER_URL="https://raw.githubusercontent.com/osheroff/onetimeserver/master/wrapper/wrapper.c" +ONETIMESERVER_URL="https://raw.githubusercontent.com/osheroff/onetimeserver-binaries/master/onetimeserver-go/$OS/onetimeserver-go" +CACHE_DIR=$HOME/.onetimeserver/$PLATFORM + +function usage() { + echo "usage: onetimeserver [--mysql-version=VERSION|-m VERSION] [-v|--verbose] [--parent-pid=PID] [--no-clean] [--reuse PATH] [mysql_args...]" + echo " onetimeserver update" +} + +while [[ $# > 0 ]] +do + case $1 in + update) + rm -Rf $CACHE_DIR/* + curl "$THIS_URL" > $0 + echo "onetimeserver updated" + exit + ;; + --parent-pid=*) + PARENT_PID="${1#*=}" + ;; + --mysql-version=*) + MYSQL_VERSION="-mysql-version ${1#*=}" + ;; + -m) + shift + MYSQL_VERSION="-mysql-version $1" + ;; + --reuse=*) + REUSE="-reuse ${1#*=}" + ;; + -b|--block) + BLOCK="1" + ;; + --no-clean) + NO_CLEAN="-no-clean" + ;; + -d|--debug|-debug) + DEBUG="-debug" + ;; + -h|--help) + usage + exit + ;; + *) + EXTRA_ARGS="$EXTRA_ARGS $1" + ;; + esac + shift + +done + +mkdir -p $CACHE_DIR + + +cd $CACHE_DIR + +if ! [ -f $CACHE_DIR/wrapper ] +then + if [ "$DEBUG" == "-debug" ] + then + echo "downloading and compiling wrapper from $WRAPPER_URL" 1>&2 + fi + + curl -s "$WRAPPER_URL" > $CACHE_DIR/wrapper.c + gcc -g -O2 $CACHE_DIR/wrapper.c -o $CACHE_DIR/wrapper +fi + +if ! [ -f $CACHE_DIR/onetimeserver-go ] +then + if [ "$DEBUG" == "-debug" ] + then + echo "downloading main util from $ONETIMESERVER_URL" 1>&2 + fi + + curl -s $ONETIMESERVER_URL > $CACHE_DIR/onetimeserver-go + + SIZE=`du -k "$CACHE_DIR/onetimeserver-go" | cut -f1` + if [ $SIZE -lt 1000 ] + then + echo "Could not download onetimeserver-go from $ONETIMESERVER_URL" >&2 + rm -Rf $CACHE_DIR/* + exit 1 + fi + + chmod +x $CACHE_DIR/onetimeserver-go +fi + +if ! [ -z "$BLOCK" ] +then + PARENT_PID=$$ +elif [ -z "$PARENT_PID" ] +then + PARENT_PID=$PPID +fi + +if [ "$DEBUG" == "-debug" ] +then + echo "exec: " $CACHE_DIR/wrapper $DEBUG $BLOCK $MYSQL_VERSION -ppid $PARENT_PID -type mysql -- $EXTRA_ARGS 1>&2 +fi + +GO_ARGS="$DEBUG $BLOCK $MYSQL_VERSION $NO_CLEAN $REUSE -ppid $PARENT_PID -type mysql" +if [ "$BLOCK" == "1" ] +then + $CACHE_DIR/wrapper $GO_ARGS -- $EXTRA_ARGS & + sleep 2000000 +else + exec $CACHE_DIR/wrapper $GO_ARGS -- $EXTRA_ARGS +fi diff --git a/src/test/resources/mysql-bin.aurora-padding b/src/test/resources/mysql-bin.aurora-padding new file mode 100644 index 00000000..688b51b2 Binary files /dev/null and b/src/test/resources/mysql-bin.aurora-padding differ diff --git a/src/test/resources/mysql-bin.compressed b/src/test/resources/mysql-bin.compressed new file mode 100644 index 00000000..3e47c177 Binary files /dev/null and b/src/test/resources/mysql-bin.compressed differ diff --git a/supplement/codequality/checkstyle.xml b/supplement/codequality/checkstyle.xml deleted file mode 100644 index 832357d4..00000000 --- a/supplement/codequality/checkstyle.xml +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/supplement/codequality/license.header b/supplement/codequality/license.header deleted file mode 100644 index a0c33261..00000000 --- a/supplement/codequality/license.header +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright [yyyy] [name of copyright owner] - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ \ No newline at end of file diff --git a/supplement/codequality/readme.md b/supplement/codequality/readme.md deleted file mode 100644 index 99c9aebb..00000000 --- a/supplement/codequality/readme.md +++ /dev/null @@ -1,5 +0,0 @@ -### IDE integration - -1. import checkstyle.xml configuration file -2. set "checkstyle.header.file" property to the absolute path of license.header file -3. add [checkstyle-nonstandard-0.1.0.jar](http://search.maven.org/remotecontent?filepath=com/github/shyiko/checkstyle-nonstandard/0.1.0/checkstyle-nonstandard-0.1.0.jar) to the list of third-party check providers \ No newline at end of file diff --git a/supplement/vagrant/mysql-5.5.27-sandbox-prepackaged/vagrantfile b/supplement/vagrant/mysql-5.5.27-sandbox-prepackaged/vagrantfile deleted file mode 100644 index 5e349bfd..00000000 --- a/supplement/vagrant/mysql-5.5.27-sandbox-prepackaged/vagrantfile +++ /dev/null @@ -1,6 +0,0 @@ -Vagrant.configure("2") do |config| - config.vm.box = 'shyiko/mysql-sandbox-prepackaged' - config.vm.box_version = '5.5.27' - config.vm.network :forwarded_port, guest: 33061, host: 33061 - config.vm.network :forwarded_port, guest: 33062, host: 33062 -end diff --git a/supplement/vagrant/mysql-5.5.27-sandbox/vagrantfile b/supplement/vagrant/mysql-5.5.27-sandbox/vagrantfile deleted file mode 100644 index ddaf1c71..00000000 --- a/supplement/vagrant/mysql-5.5.27-sandbox/vagrantfile +++ /dev/null @@ -1,21 +0,0 @@ -Vagrant.configure("2") do |config| - config.vm.box = 'lucid32' - config.vm.box_url = 'http://files.vagrantup.com/lucid32.box' - config.vm.provision :shell, :inline => %Q( - apt-get update && apt-get install -y make libaio1 # libaio1 required by mysql - echo 'Downloading MySQL distribution ...' - wget --progress=dot:mega --content-disposition \ - http://downloads.mysql.com/archives/mysql-5.5/mysql-5.5.27-linux2.6-i686.tar.gz \ - 2>&1 | grep --line-buffered -o '[0-9]*%' - wget -O - https://launchpad.net/mysql-sandbox/mysql-sandbox-3/mysql-sandbox-3/+download/MySQL-Sandbox-3.0.33.tar.gz | tar xzv - (cd MySQL-Sandbox-3.0.33 && perl Makefile.PL && make && make install) - su -c "make_replication_sandbox ~/mysql-5.5.27-linux2.6-i686.tar.gz \ - --remote_access='%' --how_many_slaves=1 --sandbox_base_port=33061 \ - --master_options='-c binlog_format=ROW' \ - --slave_options='-c binlog_format=ROW -c log-slave-updates=TRUE'" vagrant - rm -f *.tar.gz - sed -i -e "s/exit\ 0/\\/home\\/vagrant\\/sandboxes\\/rsandbox_mysql-5_5_27\\/restart_all; exit 0/g" /etc/rc.local - ) - config.vm.network :forwarded_port, guest: 33061, host: 33061 - config.vm.network :forwarded_port, guest: 33062, host: 33062 -end diff --git a/supplement/vagrant/mysql-5.6.12-sandbox-prepackaged/vagrantfile b/supplement/vagrant/mysql-5.6.12-sandbox-prepackaged/vagrantfile deleted file mode 100644 index fc15f376..00000000 --- a/supplement/vagrant/mysql-5.6.12-sandbox-prepackaged/vagrantfile +++ /dev/null @@ -1,6 +0,0 @@ -Vagrant.configure("2") do |config| - config.vm.box = 'shyiko/mysql-sandbox-prepackaged' - config.vm.box_version = '5.6.12' - config.vm.network :forwarded_port, guest: 33061, host: 33061 - config.vm.network :forwarded_port, guest: 33062, host: 33062 -end diff --git a/supplement/vagrant/mysql-5.6.12-sandbox/vagrantfile b/supplement/vagrant/mysql-5.6.12-sandbox/vagrantfile deleted file mode 100644 index 4f36dc3a..00000000 --- a/supplement/vagrant/mysql-5.6.12-sandbox/vagrantfile +++ /dev/null @@ -1,21 +0,0 @@ -Vagrant.configure("2") do |config| - config.vm.box = 'lucid32' - config.vm.box_url = 'http://files.vagrantup.com/lucid32.box' - config.vm.provision :shell, :inline => %Q( - apt-get update && apt-get install -y make libaio1 # libaio1 required by mysql - echo 'Downloading MySQL distribution ...' - wget --progress=dot:mega --content-disposition \ - http://cdn.mysql.com/Downloads/MySQL-5.6/mysql-5.6.12-linux-glibc2.5-i686.tar.gz \ - 2>&1 | grep --line-buffered -o '[0-9]*%' - wget -O - https://launchpad.net/mysql-sandbox/mysql-sandbox-3/mysql-sandbox-3/+download/MySQL-Sandbox-3.0.33.tar.gz | tar xzv - (cd MySQL-Sandbox-3.0.33 && perl Makefile.PL && make && make install) - su -c "make_replication_sandbox ~/mysql-5.6.12-linux-glibc2.5-i686.tar.gz \ - --remote_access='%' --how_many_slaves=1 --sandbox_base_port=33061 \ - --master_options='-c binlog_format=ROW' \ - --slave_options='-c binlog_format=ROW -c log-slave-updates=TRUE'" vagrant - rm -f *.tar.gz - sed -i -e "s/exit\ 0/\\/home\\/vagrant\\/sandboxes\\/rsandbox_mysql-5_6_12\\/restart_all; exit 0/g" /etc/rc.local - ) - config.vm.network :forwarded_port, guest: 33061, host: 33061 - config.vm.network :forwarded_port, guest: 33062, host: 33062 -end diff --git a/supplement/vagrant/mysql-5.7.15-sandbox-prepackaged/vagrantfile b/supplement/vagrant/mysql-5.7.15-sandbox-prepackaged/vagrantfile deleted file mode 100644 index 7a644a5c..00000000 --- a/supplement/vagrant/mysql-5.7.15-sandbox-prepackaged/vagrantfile +++ /dev/null @@ -1,6 +0,0 @@ -Vagrant.configure("2") do |config| - config.vm.box = 'shyiko/mysql-sandbox-prepackaged' - config.vm.box_version = '5.7.15' - config.vm.network :forwarded_port, guest: 33061, host: 33061 - config.vm.network :forwarded_port, guest: 33062, host: 33062 -end diff --git a/supplement/vagrant/mysql-5.7.15-sandbox/vagrantfile b/supplement/vagrant/mysql-5.7.15-sandbox/vagrantfile deleted file mode 100644 index 07cfd309..00000000 --- a/supplement/vagrant/mysql-5.7.15-sandbox/vagrantfile +++ /dev/null @@ -1,22 +0,0 @@ -Vagrant.configure("2") do |config| - config.vm.box = 'lucid32' - config.vm.box_url = 'http://files.vagrantup.com/lucid32.box' - config.vm.provision :shell, :inline => %Q( - sed -i.bak -r 's/(us.)?(archive|security).ubuntu.com/old-releases.ubuntu.com/g' /etc/apt/sources.list - apt-get update && apt-get install -y make libaio1 # libaio1 required by mysql - echo 'Downloading MySQL distribution ...' - wget --progress=dot:mega --content-disposition \ - http://cdn.mysql.com/Downloads/MySQL-5.7/mysql-5.7.15-linux-glibc2.5-i686.tar.gz \ - 2>&1 | grep --line-buffered -o '[0-9]*%' - wget -O - https://github.com/datacharmer/mysql-sandbox/releases/download/3.1.13/MySQL-Sandbox-3.1.13.tar.gz | tar xzv - (cd MySQL-Sandbox-3.1.13 && perl Makefile.PL && make && make install) - su -c "make_replication_sandbox ~/mysql-5.7.15-linux-glibc2.5-i686.tar.gz \ - --remote_access='%' --how_many_slaves=1 --sandbox_base_port=33061 \ - --master_options='-c binlog_format=ROW' \ - --slave_options='-c binlog_format=ROW -c log-slave-updates=TRUE'" vagrant - rm -f *.tar.gz - sed -i -e "s/exit\ 0/\\/home\\/vagrant\\/sandboxes\\/rsandbox_mysql-5_7_15\\/restart_all; exit 0/g" /etc/rc.local - ) - config.vm.network :forwarded_port, guest: 33061, host: 33061 - config.vm.network :forwarded_port, guest: 33062, host: 33062 -end diff --git a/supplement/vagrant/mysql-8.0.1-sandbox-prepackaged/vagrantfile b/supplement/vagrant/mysql-8.0.1-sandbox-prepackaged/vagrantfile deleted file mode 100644 index 12b10d45..00000000 --- a/supplement/vagrant/mysql-8.0.1-sandbox-prepackaged/vagrantfile +++ /dev/null @@ -1,9 +0,0 @@ -Vagrant.configure("2") do |config| - config.vm.box = 'shyiko/mysql-sandbox-prepackaged' - config.vm.box_version = '8.0.1' - config.vm.network :forwarded_port, guest: 33061, host: 33061 - config.vm.network :forwarded_port, guest: 33062, host: 33062 - config.vm.provider "virtualbox" do |v| - v.memory = 1024 - end -end diff --git a/supplement/vagrant/mysql-8.0.1-sandbox/vagrantfile b/supplement/vagrant/mysql-8.0.1-sandbox/vagrantfile deleted file mode 100644 index ba712b6a..00000000 --- a/supplement/vagrant/mysql-8.0.1-sandbox/vagrantfile +++ /dev/null @@ -1,24 +0,0 @@ -Vagrant.configure("2") do |config| - config.vm.box = 'deb/jessie-i386' - config.vm.provision :shell, :inline => %Q( - sed -i.bak -r 's/(us.)?(archive|security).ubuntu.com/old-releases.ubuntu.com/g' /etc/apt/sources.list - apt-get update && apt-get install -y make libaio1 libnuma1 libtinfo5 # lib* required by mysql - echo 'Downloading MySQL distribution ...' - wget --no-check-certificate --progress=dot:mega --content-disposition \ - https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.1-dmr-linux-glibc2.12-i686.tar.gz \ - 2>&1 | grep --line-buffered -o '[0-9]*%' - wget -O - https://github.com/datacharmer/mysql-sandbox/releases/download/3.2.13/MySQL-Sandbox-3.2.13.tar.gz | tar xzv - (cd MySQL-Sandbox-3.2.13 && perl Makefile.PL && make && make install) - su -c "make_replication_sandbox ~/mysql-8.0.1-dmr-linux-glibc2.12-i686.tar.gz \ - --remote_access='%' --how_many_slaves=1 --sandbox_base_port=33061 \ - --master_options='-c binlog_format=ROW' \ - --slave_options='-c binlog_format=ROW -c log-slave-updates=TRUE'" vagrant - rm -f *.tar.gz - sed -i -e "s/exit\ 0/\\/home\\/vagrant\\/sandboxes\\/rsandbox_mysql-8_0_1\\/restart_all; exit 0/g" /etc/rc.local - ) - config.vm.network :forwarded_port, guest: 33061, host: 33061 - config.vm.network :forwarded_port, guest: 33062, host: 33062 - config.vm.provider "virtualbox" do |v| - v.memory = 1024 - end -end diff --git a/supplement/vagrant/readme.md b/supplement/vagrant/readme.md deleted file mode 100644 index 7099541f..00000000 --- a/supplement/vagrant/readme.md +++ /dev/null @@ -1,7 +0,0 @@ -### Sandbox repackaging - -1. cd mysql-X.X.XX-sandbox -2. vagrant up -3. vagrant package --output ../mysql-X.X.XX-sandbox.box -4. vagrant box add mysql-X.X.XX-sandbox ../mysql-X.X.XX-sandbox.box --force -