├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── build.gradle
├── gradle
├── libraries.gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── install-thrift-0.9.3.sh
├── kafka-test-harness
├── build.gradle
└── src
│ └── main
│ └── scala
│ └── com
│ └── airbnb
│ └── spinaltap
│ └── kafka
│ ├── AbstractKafkaIntegrationTestHarness.scala
│ ├── AbstractKafkaServerTestHarness.scala
│ ├── AbstractZookeeperTestHarness.scala
│ ├── EmbeddedZookeeper.scala
│ └── TestUtils.scala
├── settings.gradle
├── spinaltap-common
├── build.gradle
└── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── airbnb
│ │ ├── common
│ │ └── metrics
│ │ │ ├── Counter.java
│ │ │ ├── DropwizardCounter.java
│ │ │ ├── DropwizardHistogram.java
│ │ │ ├── Histogram.java
│ │ │ ├── TaggedMetricRegistry.java
│ │ │ └── TaggedMetricRegistryFactory.java
│ │ └── spinaltap
│ │ └── common
│ │ ├── config
│ │ ├── DestinationConfiguration.java
│ │ ├── SourceConfiguration.java
│ │ └── TlsConfiguration.java
│ │ ├── destination
│ │ ├── AbstractDestination.java
│ │ ├── BufferedDestination.java
│ │ ├── Destination.java
│ │ ├── DestinationBuilder.java
│ │ ├── DestinationMetrics.java
│ │ ├── DestinationPool.java
│ │ └── ListenableDestination.java
│ │ ├── exception
│ │ ├── AttributeValueDeserializationException.java
│ │ ├── ColumnDeserializationException.java
│ │ ├── DestinationException.java
│ │ ├── EntityDeserializationException.java
│ │ ├── SourceException.java
│ │ └── SpinaltapException.java
│ │ ├── metrics
│ │ └── SpinalTapMetrics.java
│ │ ├── pipe
│ │ ├── AbstractPipeFactory.java
│ │ ├── Pipe.java
│ │ ├── PipeManager.java
│ │ └── PipeMetrics.java
│ │ ├── source
│ │ ├── AbstractDataStoreSource.java
│ │ ├── AbstractSource.java
│ │ ├── ListenableSource.java
│ │ ├── MysqlSourceState.java
│ │ ├── Source.java
│ │ ├── SourceEvent.java
│ │ ├── SourceMetrics.java
│ │ └── SourceState.java
│ │ ├── util
│ │ ├── BatchMapper.java
│ │ ├── ChainedFilter.java
│ │ ├── ClassBasedMapper.java
│ │ ├── ConcurrencyUtil.java
│ │ ├── ErrorHandler.java
│ │ ├── Filter.java
│ │ ├── Joiner.java
│ │ ├── JsonUtil.java
│ │ ├── KeyProvider.java
│ │ ├── Mapper.java
│ │ ├── Repository.java
│ │ ├── StateRepositoryFactory.java
│ │ ├── ThreeWayJoiner.java
│ │ ├── Validator.java
│ │ └── ZookeeperRepository.java
│ │ └── validator
│ │ └── MutationOrderValidator.java
│ └── test
│ └── java
│ └── com
│ └── airbnb
│ └── spinaltap
│ └── common
│ ├── destination
│ ├── AbstractDestinationTest.java
│ ├── BufferedDestinationTest.java
│ ├── DestinationBuilderTest.java
│ ├── DestinationPoolTest.java
│ └── ListenableDestinationTest.java
│ ├── pipe
│ ├── PipeManagerTest.java
│ └── PipeTest.java
│ ├── source
│ ├── AbstractSourceTest.java
│ └── ListenableSourceTest.java
│ ├── util
│ ├── ChainedFilterTest.java
│ └── ClassBasedMapperTest.java
│ └── validator
│ └── MutationOrderValidatorTest.java
├── spinaltap-kafka
├── build.gradle
└── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── airbnb
│ │ └── spinaltap
│ │ └── kafka
│ │ ├── KafkaDestination.java
│ │ ├── KafkaDestinationBuilder.java
│ │ └── KafkaProducerConfiguration.java
│ └── test
│ └── java
│ └── com
│ └── airbnb
│ └── spinaltap
│ └── kafka
│ └── KafkaDestinationTest.java
├── spinaltap-model
├── build.gradle
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── airbnb
│ │ │ └── spinaltap
│ │ │ ├── Mutation.java
│ │ │ └── mysql
│ │ │ ├── BinlogFilePos.java
│ │ │ ├── DataSource.java
│ │ │ ├── GtidSet.java
│ │ │ ├── Transaction.java
│ │ │ └── mutation
│ │ │ ├── MysqlDeleteMutation.java
│ │ │ ├── MysqlInsertMutation.java
│ │ │ ├── MysqlMutation.java
│ │ │ ├── MysqlMutationMetadata.java
│ │ │ ├── MysqlUpdateMutation.java
│ │ │ └── schema
│ │ │ ├── Column.java
│ │ │ ├── ColumnDataType.java
│ │ │ ├── ColumnMetadata.java
│ │ │ ├── PrimaryKey.java
│ │ │ ├── Row.java
│ │ │ └── Table.java
│ └── thrift
│ │ └── com
│ │ └── airbnb
│ │ └── jitney
│ │ └── event
│ │ └── spinaltap
│ │ └── spinaltap_v1.thrift
│ └── test
│ └── java
│ └── com
│ └── airbnb
│ └── spinaltap
│ ├── GtidSetTest.java
│ ├── MutationTest.java
│ └── mysql
│ ├── BinlogFilePosTest.java
│ ├── mutation
│ └── MysqlUpdateMutationTest.java
│ └── schema
│ └── RowTest.java
├── spinaltap-mysql
├── build.gradle
└── src
│ ├── main
│ ├── antlr
│ │ └── com
│ │ │ └── airbnb
│ │ │ └── spinaltap
│ │ │ └── mysql
│ │ │ └── schema
│ │ │ └── MySQL.g4
│ └── java
│ │ └── com
│ │ └── airbnb
│ │ └── spinaltap
│ │ └── mysql
│ │ ├── ColumnSerializationUtil.java
│ │ ├── MysqlClient.java
│ │ ├── MysqlDestinationMetrics.java
│ │ ├── MysqlPipeFactory.java
│ │ ├── MysqlSource.java
│ │ ├── MysqlSourceFactory.java
│ │ ├── MysqlSourceMetrics.java
│ │ ├── StateHistory.java
│ │ ├── StateRepository.java
│ │ ├── TableCache.java
│ │ ├── binlog_connector
│ │ ├── BinaryLogConnectorEventMapper.java
│ │ └── BinaryLogConnectorSource.java
│ │ ├── config
│ │ ├── AbstractMysqlConfiguration.java
│ │ ├── MysqlConfiguration.java
│ │ └── MysqlSchemaStoreConfiguration.java
│ │ ├── event
│ │ ├── BinlogEvent.java
│ │ ├── DeleteEvent.java
│ │ ├── GTIDEvent.java
│ │ ├── QueryEvent.java
│ │ ├── StartEvent.java
│ │ ├── TableMapEvent.java
│ │ ├── UpdateEvent.java
│ │ ├── WriteEvent.java
│ │ ├── XidEvent.java
│ │ ├── filter
│ │ │ ├── DuplicateFilter.java
│ │ │ ├── EventTypeFilter.java
│ │ │ ├── MysqlEventFilter.java
│ │ │ └── TableFilter.java
│ │ └── mapper
│ │ │ ├── DeleteMutationMapper.java
│ │ │ ├── GTIDMapper.java
│ │ │ ├── InsertMutationMapper.java
│ │ │ ├── MysqlMutationMapper.java
│ │ │ ├── QueryMapper.java
│ │ │ ├── StartMapper.java
│ │ │ ├── TableMapMapper.java
│ │ │ ├── UpdateMutationMapper.java
│ │ │ └── XidMapper.java
│ │ ├── exception
│ │ └── InvalidBinlogPositionException.java
│ │ ├── mutation
│ │ ├── MysqlKeyProvider.java
│ │ └── mapper
│ │ │ ├── DeleteMutationMapper.java
│ │ │ ├── InsertMutationMapper.java
│ │ │ ├── ThriftMutationMapper.java
│ │ │ └── UpdateMutationMapper.java
│ │ ├── schema
│ │ ├── MysqlColumn.java
│ │ ├── MysqlSchemaArchiver.java
│ │ ├── MysqlSchemaDatabase.java
│ │ ├── MysqlSchemaManager.java
│ │ ├── MysqlSchemaManagerFactory.java
│ │ ├── MysqlSchemaReader.java
│ │ ├── MysqlSchemaStore.java
│ │ ├── MysqlSchemaUtil.java
│ │ └── MysqlTableSchema.java
│ │ └── validator
│ │ ├── EventOrderValidator.java
│ │ └── MutationSchemaValidator.java
│ └── test
│ └── java
│ └── com
│ └── airbnb
│ └── spinaltap
│ └── mysql
│ ├── ColumnSerializationUtilTest.java
│ ├── EventOrderValidatorTest.java
│ ├── MysqlSourceTest.java
│ ├── StateHistoryTest.java
│ ├── StateRepositoryTest.java
│ ├── TableCacheTest.java
│ ├── binlog_connector
│ └── BinaryLogConnectorEventMapperTest.java
│ ├── event
│ ├── filter
│ │ └── MysqlEventFilterTest.java
│ └── mapper
│ │ └── MysqlMutationMapperTest.java
│ ├── mutation
│ └── MysqlKeyProviderTest.java
│ ├── schema
│ ├── MysqlColumnTest.java
│ ├── MysqlSchemaDatabaseTest.java
│ ├── MysqlSchemaManagerTest.java
│ ├── MysqlSchemaStoreTest.java
│ └── MysqlSchemaUtilTest.java
│ ├── util
│ └── JsonSerializationTest.java
│ └── validator
│ └── MutationSchemaValidatorTest.java
└── spinaltap-standalone
├── build.gradle
└── src
└── main
├── java
└── com
│ └── airbnb
│ └── spinaltap
│ ├── SpinalTapStandaloneApp.java
│ ├── SpinalTapStandaloneConfiguration.java
│ └── ZookeeperRepositoryFactory.java
└── resources
├── log4j2.yaml
└── spinaltap-standalone-config-sample.yaml
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | **/build/
3 | classes
4 | thrift-gen/
5 |
6 | GIT_CHECKOUT_PATHS
7 |
8 | # Gradle
9 | .gradle/
10 | **/.gradle/
11 | **/.gradletasknamecache
12 |
13 | # VIM
14 | *.swp
15 | *~
16 |
17 | # Eclipse
18 | workbench.xmi
19 |
20 | .idea/
21 | *.iml
22 | *.ipr
23 | *.iws
24 | **/*.iml
25 | out/
26 | .classpath
27 | **/.classpath
28 | .project
29 | **/.project
30 | .settings/
31 | **/.settings/
32 | # Ignore OS X Desktop Services store
33 | .DS_Store
34 | *.DS_Store
35 | **/.DS_Store
36 | .tddium*
37 | **/*~
38 |
39 | # NetBeans
40 | .nb-gradle/
41 | .solano*
42 |
43 | # Bazel
44 | WORKSPACE
45 | **/BUILD
46 | third_party/
47 | /bazel-bazel
48 | /bazel-bin
49 | /bazel-genfiles
50 | /bazel-io_bazel
51 | /bazel-out
52 | /bazel-testlogs
53 | /bazel.iml
54 |
55 | # VSCode
56 | .vscode/
57 |
58 | # Gradle Enterprise
59 | .build_scan_info.json
60 |
61 | # log files
62 | *.log
63 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 |
3 | dist: bionic
4 |
5 | sudo: true
6 |
7 | before_install:
8 | - sudo apt-get -qq update
9 | - sudo apt-get install -y automake bison flex g++ git libboost-all-dev libevent-dev libssl1.0-dev libtool make pkg-config openjdk-8-jdk
10 | - ./install-thrift-0.9.3.sh
11 | - wget https://raw.githubusercontent.com/michaelklishin/jdk_switcher/master/jdk_switcher.sh
12 | - source jdk_switcher.sh
13 | - jdk_switcher use openjdk8
14 |
15 | install:
16 | - ./gradlew spotlessCheck
17 | - ./gradlew assemble
18 |
19 | script:
20 | - ./gradlew check
21 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing to SpinalTap
2 |
3 | ### General Guideline
4 | - Please fork this repository (https://github.com/airbnb/SpinalTap) and make changes on the fork.
5 | - Please include corresponding unit/functional/integration tests if you are committing a non-trivial change.
6 | - Please run the `gradle spotlessApply` in the root directory to follow the code style.
7 | - Please do NOT change any public APIs without discussing with the team.
8 | - Please follow the `Design Template` to document the new feature design, and send a PR for review.
9 |
10 | ### Design Template
11 | - Motivation
12 | [Discuss the existing pain points and the advantages will get through the new features.]
13 | - Public Interface
14 | [Discuss what are the changes on the public APIs (newly added, deprecated, and deleted APIs).]
15 | - Proposed Changes
16 | [Discuss in details about the proposed changes.]
17 | - Compatibility, Deprecation, and Migration Plan
18 | [Discuss the potential compatibility issues and any migration process we need to follow if there is any.]
19 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | apply from: 'gradle/libraries.gradle'
2 | buildscript {
3 | repositories {
4 | maven {
5 | url "https://plugins.gradle.org/m2/"
6 | }
7 | }
8 | dependencies {
9 | classpath "com.diffplug.spotless:spotless-plugin-gradle:3.8.0"
10 | }
11 | }
12 |
13 | subprojects {
14 | apply plugin: 'java'
15 | apply plugin: 'com.diffplug.gradle.spotless'
16 | repositories {
17 | mavenCentral()
18 | }
19 |
20 | compileJava {
21 | sourceCompatibility = '1.8'
22 | targetCompatibility = '1.8'
23 |
24 | options.compilerArgs << '-Xlint'
25 | }
26 |
27 | spotless {
28 | enforceCheck = false
29 | java {
30 | licenseHeader '/** Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license information. */'
31 | googleJavaFormat('1.4')
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/gradle/libraries.gradle:
--------------------------------------------------------------------------------
1 | ext {
2 | libraries = [
3 | antlr4: 'org.antlr:antlr4:4.7.1',
4 | apache_commons_lang: 'org.apache.commons:commons-lang3:3.7',
5 | apache_curator_framework: 'org.apache.curator:curator-framework:4.0.1',
6 | codahale_metrics_core: 'com.codahale.metrics:metrics-core:3.0.2',
7 | findbugs_jsr305: 'com.google.code.findbugs:jsr305:3.0.0',
8 | guava: 'com.google.guava:guava:22.0',
9 | guava_retrying: 'com.github.rholder:guava-retrying:1.0.6',
10 | hibernate_validator: 'org.hibernate:hibernate-validator:5.1.3.Final',
11 | icu4j: 'com.ibm.icu:icu4j:61.1',
12 | jackson_annotations: 'com.fasterxml.jackson.core:jackson-annotations:2.9.5',
13 | jackson_databind: 'com.fasterxml.jackson.core:jackson-databind:2.9.5',
14 | jackson_dataformat_yaml: 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.5',
15 | jackson_datatype_joda: 'com.fasterxml.jackson.datatype:jackson-datatype-joda:2.9.5',
16 | jackson_datatype_guava: 'com.fasterxml.jackson.datatype:jackson-datatype-guava:2.9.5',
17 | javax_validaton: 'javax.validation:validation-api:1.1.0.Final',
18 | jdbi3: 'org.jdbi:jdbi3-core:3.10.1',
19 | junit: 'junit:junit:4.12',
20 | kafka_core: 'org.apache.kafka:kafka_2.10:0.9.0.1',
21 | kafka_clients: 'org.apache.kafka:kafka-clients:0.9.0.1',
22 | kafka_test: 'org.apache.kafka:kafka_2.10:0.9.0.1:test',
23 | log4j_slf4j_impl: 'org.apache.logging.log4j:log4j-slf4j-impl:2.9.1',
24 | lombok: 'org.projectlombok:lombok:1.18.2',
25 | mokito: 'org.mockito:mockito-all:1.10.19',
26 | mysql_binlog_connector: 'com.github.shyiko:mysql-binlog-connector-java:0.20.1',
27 | mysql_connector: 'mysql:mysql-connector-java:5.1.44',
28 | slf4j: 'org.slf4j:slf4j-api:1.7.25',
29 | thrift: 'org.apache.thrift:libthrift:0.9.3',
30 | zookeeper: 'org.apache.zookeeper:zookeeper:3.4.11'
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/SpinalTap/6992bc3352a9ecf74ca40ec93c9c56bbbc5dc413/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip
6 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/install-thrift-0.9.3.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -ex
3 | wget http://archive.apache.org/dist/thrift/0.9.3/thrift-0.9.3.tar.gz
4 | tar xzf thrift-0.9.3.tar.gz
5 | cd thrift-0.9.3
6 | ./configure --without-qt4 --without-qt5 --without-csharp --without-erlang --without-nodejs --without-lua --without-python --without-perl --without-php --without-php_extension --without-dart --without-ruby --without-haskell --without-go --without-haxe --without-d
7 | sed -i -e 's#mvn.repo=http://repo1.maven.org/maven2#mvn.repo=https://repo1.maven.org/maven2#g' ./lib/java/build.properties
8 | make
9 | sudo make install
10 |
--------------------------------------------------------------------------------
/kafka-test-harness/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java'
2 | apply plugin: 'scala'
3 |
4 | dependencies {
5 | compile libraries.kafka_core
6 | compile libraries.kafka_clients
7 | compile libraries.kafka_test
8 | }
9 |
--------------------------------------------------------------------------------
/kafka-test-harness/src/main/scala/com/airbnb/spinaltap/kafka/AbstractKafkaIntegrationTestHarness.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information.
3 | */
4 |
5 | package com.airbnb.spinaltap.kafka
6 |
7 | import java.util.Properties
8 |
9 | import kafka.server.KafkaConfig
10 |
11 | /**
12 | * LinkedIn integration test harness for Kafka
13 | * This is simply a copy of open source code, we do this because java does not support trait, we are making it abstract
14 | * class so user java test class can extend it.
15 | */
16 | abstract class AbstractKafkaIntegrationTestHarness extends AbstractKafkaServerTestHarness {
17 |
18 | def generateConfigs() =
19 | TestUtils.createBrokerConfigs(clusterSize(), zkConnect, enableControlledShutdown = false).map(KafkaConfig.fromProps(_, overridingProps()))
20 |
21 | /**
22 | * User can override this method to return the number of brokers they want.
23 | * By default only one broker will be launched.
24 | * @return the number of brokers needed in the Kafka cluster for the test.
25 | */
26 | def clusterSize(): Int = 1
27 |
28 | /**
29 | * User can override this method to apply customized configurations to the brokers.
30 | * By default the only configuration is number of partitions when topics get automatically created. The default value
31 | * is 1.
32 | * @return The configurations to be used by brokers.
33 | */
34 | def overridingProps(): Properties = {
35 | val props = new Properties()
36 | props.setProperty(KafkaConfig.NumPartitionsProp, 1.toString)
37 | props
38 | }
39 |
40 | /**
41 | * Returns the bootstrap servers configuration string to be used by clients.
42 | * @return bootstrap servers string.
43 | */
44 | def bootstrapServers(): String = super.bootstrapUrl
45 |
46 | /**
47 | * This method should be defined as @beforeMethod.
48 | */
49 | override def setUp() {
50 | super.setUp()
51 | }
52 |
53 | /**
54 | * This method should be defined as @AfterMethod.
55 | */
56 | override def tearDown() {
57 | super.tearDown()
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/kafka-test-harness/src/main/scala/com/airbnb/spinaltap/kafka/AbstractKafkaServerTestHarness.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information.
3 | */
4 |
5 | package com.airbnb.spinaltap.kafka
6 |
7 | import java.io.File
8 | import java.util
9 | import java.util.Properties
10 |
11 | import kafka.common.KafkaException
12 | import kafka.server.{KafkaConfig, KafkaServer}
13 | import kafka.utils.CoreUtils
14 | import org.apache.kafka.common.protocol.SecurityProtocol
15 | import org.apache.kafka.common.security.auth.KafkaPrincipal
16 |
17 | import scala.collection.mutable
18 |
19 | /**
20 | * Kafka server integration test harness.
21 | * This is simply a copy of open source code, we do this because java does not support trait, we are making it abstract
22 | * class so user java test class can extend it.
23 | */
24 | abstract class AbstractKafkaServerTestHarness extends AbstractZookeeperTestHarness {
25 | var instanceConfigs: Seq[KafkaConfig] = null
26 | var servers: mutable.Buffer[KafkaServer] = null
27 | var brokerList: String = null
28 | var alive: Array[Boolean] = null
29 | val kafkaPrincipalType = KafkaPrincipal.USER_TYPE
30 | val setClusterAcl: Option[() => Unit] = None
31 |
32 | /**
33 | * Implementations must override this method to return a set of KafkaConfigs. This method will be invoked for every
34 | * test and should not reuse previous configurations unless they select their ports randomly when servers are started.
35 | */
36 | def generateConfigs(): Seq[KafkaConfig]
37 |
38 | def serverForId(id: Int) = servers.find(s => s.config.brokerId == id)
39 |
40 | def bootstrapUrl: String = brokerList
41 |
42 | protected def securityProtocol: SecurityProtocol = SecurityProtocol.PLAINTEXT
43 |
44 | protected def trustStoreFile: Option[File] = None
45 |
46 | protected def saslProperties: Option[Properties] = None
47 |
48 | override def setUp() {
49 | super.setUp()
50 | val configs = generateConfigs()
51 | if (configs.size <= 0)
52 | throw new KafkaException("Must supply at least one server config.")
53 | servers = configs.map(TestUtils.createServer(_)).toBuffer
54 | brokerList = TestUtils.getBrokerListStrFromServers(servers, securityProtocol)
55 | alive = new Array[Boolean](servers.length)
56 | util.Arrays.fill(alive, true)
57 | // We need to set a cluster ACL in some cases here
58 | // because of the topic creation in the setup of
59 | // IntegrationTestHarness. If we don't, then tests
60 | // fail with a cluster action authorization exception
61 | // when processing an update metadata request
62 | // (controller -> broker).
63 | //
64 | // The following method does nothing by default, but
65 | // if the test case requires setting up a cluster ACL,
66 | // then it needs to be implemented.
67 | setClusterAcl.foreach(_.apply())
68 | }
69 |
70 | override def tearDown() {
71 | if (servers != null) {
72 | servers.foreach(_.shutdown())
73 | servers.foreach(server => CoreUtils.rm(server.config.logDirs))
74 | }
75 | super.tearDown()
76 | }
77 |
78 | /**
79 | * Pick a broker at random and kill it if it isn't already dead
80 | * Return the id of the broker killed
81 | */
82 | def killRandomBroker(): Int = {
83 | val index = TestUtils.random.nextInt(servers.length)
84 | if (alive(index)) {
85 | servers(index).shutdown()
86 | servers(index).awaitShutdown()
87 | alive(index) = false
88 | }
89 | index
90 | }
91 |
92 | /**
93 | * Restart any dead brokers
94 | */
95 | def restartDeadBrokers() {
96 | for (i <- 0 until servers.length if !alive(i)) {
97 | servers(i).startup()
98 | alive(i) = true
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/kafka-test-harness/src/main/scala/com/airbnb/spinaltap/kafka/AbstractZookeeperTestHarness.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information.
3 | */
4 |
5 | package com.airbnb.spinaltap.kafka
6 |
7 | import javax.security.auth.login.Configuration
8 |
9 | import kafka.utils.{CoreUtils, Logging, ZkUtils}
10 | import org.apache.kafka.common.security.JaasUtils
11 | import java.io.IOException
12 | import java.net.{SocketTimeoutException, Socket, InetAddress, InetSocketAddress}
13 |
14 | /**
15 | * Zookeeper test harness.
16 | * This is simply a copy of open source code, we do this because java does not support trait, we are making it abstract
17 | * class so user java test class can extend it.
18 | */
19 | abstract class AbstractZookeeperTestHarness extends Logging {
20 |
21 | val zkConnectionTimeout = 6000
22 | val zkSessionTimeout = 6000
23 |
24 | var zkUtils: ZkUtils = null
25 | var zookeeper: EmbeddedZookeeper = null
26 |
27 | def zkPort: Int = zookeeper.port
28 |
29 | def zkConnect: String = s"127.0.0.1:$zkPort"
30 |
31 | def setUp() {
32 | zookeeper = new EmbeddedZookeeper()
33 | zkUtils = ZkUtils(zkConnect, zkSessionTimeout, zkConnectionTimeout, JaasUtils.isZkSecurityEnabled)
34 | }
35 |
36 | def tearDown() {
37 | if (zkUtils != null)
38 | CoreUtils.swallow(zkUtils.close())
39 | if (zookeeper != null)
40 | CoreUtils.swallow(zookeeper.shutdown())
41 |
42 | def isDown: Boolean = {
43 | try {
44 | sendStat("127.0.0.1", zkPort, 3000)
45 | false
46 | } catch {
47 | case _: Throwable =>
48 | debug("Server is down")
49 | true
50 | }
51 | }
52 |
53 | Iterator.continually(isDown).exists(identity)
54 |
55 | Configuration.setConfiguration(null)
56 | }
57 |
58 | /**
59 | * Copied from kafka.zk.ZkFourLetterWords.scala which is NOT included in the kafka-clients version
60 | * we are using.
61 | */
62 | def sendStat(host: String, port: Int, timeout: Int) {
63 | val hostAddress =
64 | if (host != null) new InetSocketAddress(host, port)
65 | else new InetSocketAddress(InetAddress.getByName(null), port)
66 | val sock = new Socket()
67 | try {
68 | sock.connect(hostAddress, timeout)
69 | val outStream = sock.getOutputStream
70 | outStream.write("stat".getBytes)
71 | outStream.flush()
72 | } catch {
73 | case e: SocketTimeoutException => throw new IOException("Exception while sending 4lw")
74 | } finally {
75 | sock.close
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/kafka-test-harness/src/main/scala/com/airbnb/spinaltap/kafka/EmbeddedZookeeper.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information.
3 | */
4 |
5 | package com.airbnb.spinaltap.kafka
6 |
7 | import java.net.InetSocketAddress
8 |
9 | import kafka.utils.CoreUtils
10 | import org.apache.kafka.common.utils.Utils
11 | import org.apache.zookeeper.server.{NIOServerCnxnFactory, ZooKeeperServer}
12 |
13 | /**
14 | * This is a copy of open source Kafka embedded zookeeper class. We put it here to avoid dependency on o.a.k.test jar.
15 | */
16 | class EmbeddedZookeeper() {
17 | val snapshotDir = TestUtils.tempDir()
18 | val logDir = TestUtils.tempDir()
19 | val tickTime = 500
20 | val zookeeper = new ZooKeeperServer(snapshotDir, logDir, tickTime)
21 | val factory = new NIOServerCnxnFactory()
22 | private val addr = new InetSocketAddress("127.0.0.1", TestUtils.RandomPort)
23 | factory.configure(addr, 0)
24 | factory.startup(zookeeper)
25 | val port = zookeeper.getClientPort
26 |
27 | def shutdown() {
28 | CoreUtils.swallow(zookeeper.shutdown())
29 | CoreUtils.swallow(factory.shutdown())
30 | Utils.delete(logDir)
31 | Utils.delete(snapshotDir)
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include "spinaltap-common"
2 | include "spinaltap-kafka"
3 | include "spinaltap-model"
4 | include "spinaltap-mysql"
5 | include "spinaltap-standalone"
6 | include "kafka-test-harness"
--------------------------------------------------------------------------------
/spinaltap-common/build.gradle:
--------------------------------------------------------------------------------
1 | dependencies {
2 | compile project(':spinaltap-model')
3 | compile libraries.apache_commons_lang
4 | compile libraries.codahale_metrics_core
5 | compile libraries.findbugs_jsr305
6 | compile libraries.icu4j
7 | compile libraries.jackson_databind
8 | compile libraries.jackson_datatype_joda
9 | compile libraries.javax_validaton
10 | compile libraries.log4j_slf4j_impl
11 | compile libraries.zookeeper
12 | compileOnly libraries.lombok
13 | annotationProcessor libraries.lombok
14 |
15 | // Running in ZooKeeper 3.4.x compatibility mode
16 | compile(libraries.apache_curator_framework) {
17 | exclude group: 'org.apache.zookeeper', module: 'zookeeper'
18 | }
19 |
20 | testCompile libraries.junit
21 | testCompile libraries.mokito
22 | testCompileOnly libraries.lombok
23 | testAnnotationProcessor libraries.lombok
24 | }
25 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/common/metrics/Counter.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.common.metrics;
6 |
7 | /** A monotone counter */
8 | public interface Counter {
9 | /** Increment the counter by one. */
10 | public void inc();
11 |
12 | /**
13 | * Increment the counter by {@code n}.
14 | *
15 | * @param n the amount by which the counter will be increased
16 | */
17 | public void inc(long n);
18 | }
19 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/common/metrics/DropwizardCounter.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.common.metrics;
6 |
7 | /**
8 | * Our own version of the Counter class that multiplexes tagged metrics to tagged and non-tagged
9 | * versions, because Datadog is not capable of averaging across tag values correctly.
10 | */
11 | public class DropwizardCounter implements Counter {
12 |
13 | private final com.codahale.metrics.Counter[] counters;
14 |
15 | public DropwizardCounter(com.codahale.metrics.Counter... counters) {
16 | this.counters = counters;
17 | }
18 |
19 | /** Increment the counter by one. */
20 | public void inc() {
21 | inc(1);
22 | }
23 |
24 | /**
25 | * Increment the counter by {@code n}.
26 | *
27 | * @param n the amount by which the counter will be increased
28 | */
29 | public void inc(long n) {
30 | for (com.codahale.metrics.Counter counter : counters) {
31 | counter.inc(n);
32 | }
33 | }
34 |
35 | /** Decrement the counter by one. */
36 | public void dec() {
37 | dec(1);
38 | }
39 |
40 | /**
41 | * Decrement the counter by {@code n}.
42 | *
43 | * @param n the amount by which the counter will be decreased
44 | */
45 | public void dec(long n) {
46 | for (com.codahale.metrics.Counter counter : counters) {
47 | counter.dec(n);
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/common/metrics/DropwizardHistogram.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.common.metrics;
6 |
7 | /**
8 | * Our own version of the Histogram class that multiplexes tagged metrics to tagged and non-tagged
9 | * versions, because Datadog is not capable of averaging across tag values correctly.
10 | */
11 | public class DropwizardHistogram implements Histogram {
12 |
13 | private final com.codahale.metrics.Histogram[] histograms;
14 |
15 | public DropwizardHistogram(com.codahale.metrics.Histogram... histograms) {
16 | this.histograms = histograms;
17 | }
18 |
19 | /**
20 | * Adds a recorded value.
21 | *
22 | * @param value the length of the value
23 | */
24 | public void update(int value) {
25 | update((long) value);
26 | }
27 |
28 | /**
29 | * Adds a recorded value.
30 | *
31 | * @param value the length of the value
32 | */
33 | public void update(long value) {
34 | for (com.codahale.metrics.Histogram histogram : histograms) {
35 | histogram.update(value);
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/common/metrics/Histogram.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.common.metrics;
6 |
7 | /** A Histogram interface used to record and analyze the distribution of values. */
8 | public interface Histogram {
9 | /**
10 | * Adds a recorded value.
11 | *
12 | * @param value the length of the value
13 | */
14 | public void update(long value);
15 | }
16 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/common/metrics/TaggedMetricRegistryFactory.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.common.metrics;
6 |
7 | import lombok.NonNull;
8 | import lombok.extern.slf4j.Slf4j;
9 |
10 | @Slf4j
11 | /**
12 | * This provides an easy way to get the TaggedMetricRegistry in any class. The initialize must be
13 | * called before getting the TaggedMetricRegistry, otherwise the metrics will be lost.
14 | *
15 | *
The TaggedMetricRegistry is also available for injection. Consider injecting it directly in
16 | * case you want metrics during object initialization or as a general dependency injection best
17 | * practice.
18 | */
19 | public class TaggedMetricRegistryFactory {
20 |
21 | private static volatile TaggedMetricRegistry registry =
22 | TaggedMetricRegistry.NON_INITIALIZED_TAGGED_METRIC_REGISTRY;
23 |
24 | private TaggedMetricRegistryFactory() {}
25 |
26 | public static void initialize(@NonNull TaggedMetricRegistry taggedMetricRegistry) {
27 | registry = taggedMetricRegistry;
28 | }
29 |
30 | public static TaggedMetricRegistry get() {
31 | if (registry == TaggedMetricRegistry.NON_INITIALIZED_TAGGED_METRIC_REGISTRY) {
32 | log.warn(
33 | "get() called before metrics is initialized. return NON_INITIALIZED_TAGGED_METRIC_REGISTRY.");
34 | }
35 | return registry;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/config/DestinationConfiguration.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.config;
6 |
7 | import com.fasterxml.jackson.annotation.JsonProperty;
8 | import java.util.Map;
9 | import javax.validation.constraints.Min;
10 | import lombok.Data;
11 | import lombok.NoArgsConstructor;
12 | import lombok.NonNull;
13 |
14 | /** Represents a {@link com.airbnb.spinaltap.common.destination.Destination} configuration. */
15 | @Data
16 | @NoArgsConstructor
17 | public class DestinationConfiguration {
18 | public static final String DEFAULT_TYPE = "kafka";
19 | public static final int DEFAULT_BUFFER_SIZE = 0;
20 | public static final int DEFAULT_POOL_SIZE = 0;
21 |
22 | /** The destination type. Default to "kafka". */
23 | @JsonProperty("type")
24 | @NonNull
25 | private String type = DEFAULT_TYPE;
26 |
27 | /**
28 | * The buffer size. If greater than 0, a {@link
29 | * com.airbnb.spinaltap.common.destination.BufferedDestination} will be constructed.
30 | */
31 | @Min(0)
32 | @JsonProperty("buffer_size")
33 | private int bufferSize = DEFAULT_BUFFER_SIZE;
34 |
35 | /**
36 | * The pool size. If greater than 0, a {@link
37 | * com.airbnb.spinaltap.common.destination.DestinationPool} will be constructed with the specified
38 | * number of {@link com.airbnb.spinaltap.common.destination.Destination}s.
39 | */
40 | @Min(0)
41 | @JsonProperty("pool_size")
42 | private int poolSize = DEFAULT_POOL_SIZE;
43 |
44 | @JsonProperty("producer_config")
45 | private Map producerConfig;
46 | }
47 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/config/SourceConfiguration.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.config;
6 |
7 | import com.fasterxml.jackson.annotation.JsonProperty;
8 | import javax.validation.constraints.Min;
9 | import javax.validation.constraints.NotNull;
10 | import lombok.AllArgsConstructor;
11 | import lombok.Data;
12 | import lombok.NoArgsConstructor;
13 | import lombok.NonNull;
14 |
15 | /** Represents a {@code Source} configuration. */
16 | @Data
17 | @NoArgsConstructor
18 | @AllArgsConstructor
19 | public class SourceConfiguration {
20 | private static int DEFAULT_REPLICAS = 3;
21 | private static int DEFAULT_PARTITIONS = 1;
22 |
23 | public SourceConfiguration(
24 | @NonNull final String name,
25 | final String type,
26 | final String instanceTag,
27 | @NonNull final DestinationConfiguration destinationConfiguration) {
28 | this.name = name;
29 | this.type = type;
30 | this.instanceGroupTag = instanceTag;
31 | this.destinationConfiguration = destinationConfiguration;
32 | }
33 |
34 | public SourceConfiguration(String type, String instanceTag) {
35 | this.type = type;
36 | this.instanceGroupTag = instanceTag;
37 | }
38 |
39 | /** The source name. */
40 | @NotNull @JsonProperty private String name;
41 |
42 | /**
43 | * The number of replicas to stream from the source.
44 | *
45 | *
Note: This is only applicable if a cluster solution is employed. A Master-Replica state
46 | * transition model is recommended, where one cluster instance (master) is streaming events from a
47 | * given source at any point in time. This is required to ensure ordering guarantees. Replicas
48 | * will be promoted to Master in case of failure. Increasing number of replicas can be used to
49 | * improve fault tolerance.
50 | */
51 | @Min(1)
52 | @JsonProperty
53 | private int replicas = DEFAULT_REPLICAS;
54 |
55 | /**
56 | * The number of stream partitions for a given source.
57 | *
58 | *
Note: This is only applicable if a cluster solution is employed.
59 | */
60 | @Min(1)
61 | @JsonProperty
62 | private int partitions = DEFAULT_PARTITIONS;
63 |
64 | /** The source type (ex: MySQL, DynamoDB) */
65 | @JsonProperty("type")
66 | private String type;
67 |
68 | /**
69 | * The group tag for cluster instances of the given source.
70 | *
71 | *
Note: On failure, streaming should be halted and the error propagated to avoid potential.
27 | * event loss
28 | */
29 | void send(List extends Mutation>> mutations);
30 |
31 | /** Adds a {@link Listener} to the destination. */
32 | void addListener(Listener listener);
33 |
34 | /** Removes a {@link Listener} from the destination. */
35 | void removeListener(Listener listener);
36 |
37 | /** @return whether the destination is started and publishing {@link Mutation}s */
38 | boolean isStarted();
39 |
40 | /**
41 | * Initializes the destination and prepares for {@link Mutation} publishing.
42 | *
43 | *
The operation should be idempotent.
44 | */
45 | void open();
46 |
47 | /**
48 | * Stops {@link Mutation} publishing and closes the destination.
49 | *
50 | *
The operation should be idempotent.
51 | */
52 | void close();
53 |
54 | /**
55 | * Clears the state of the destination.
56 | *
57 | *
The operation should be idempotent.
58 | */
59 | void clear();
60 |
61 | /** Represents a destination listener to get notified of events and lifecycle changes. */
62 | abstract class Listener {
63 | /** Action to perform after the {@link Destination} has started. */
64 | public void onStart() {}
65 |
66 | /** Action to perform after a list of {@link Mutation}s has been published. */
67 | public void onSend(List extends Mutation>> mutations) {}
68 |
69 | /** Action to perform when an error is caught on send. */
70 | public void onError(Exception ex) {}
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/destination/ListenableDestination.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.destination;
6 |
7 | import com.airbnb.spinaltap.Mutation;
8 | import java.util.ArrayList;
9 | import java.util.List;
10 | import lombok.NonNull;
11 |
12 | /**
13 | * Base {@link Destination} implement using observer pattern to allow listening to
15 | * streamed events and subscribe to lifecycle change notifications.
16 | */
17 | abstract class ListenableDestination implements Destination {
18 | private final List listeners = new ArrayList<>();
19 |
20 | @Override
21 | public void addListener(@NonNull final Listener listener) {
22 | listeners.add(listener);
23 | }
24 |
25 | @Override
26 | public void removeListener(@NonNull final Listener listener) {
27 | listeners.remove(listener);
28 | }
29 |
30 | protected void notifyStart() {
31 | listeners.forEach(Destination.Listener::onStart);
32 | }
33 |
34 | protected void notifySend(final List extends Mutation>> mutations) {
35 | listeners.forEach(listener -> listener.onSend(mutations));
36 | }
37 |
38 | protected void notifyError(final Exception ex) {
39 | listeners.forEach(listener -> listener.onError(ex));
40 | }
41 |
42 | @Override
43 | public void open() {
44 | notifyStart();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/exception/AttributeValueDeserializationException.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.exception;
6 |
7 | /** Exception thrown when a DynamoDB attribute value cannot be deserialized. */
8 | public final class AttributeValueDeserializationException extends EntityDeserializationException {
9 | private static final long serialVersionUID = 2442564527939878665L;
10 |
11 | private static String createMessage(String attributeName, String tableName) {
12 | return String.format(
13 | "Could not deserialize thrift bytebuffer for DynamoDB attribute %s in table %s.",
14 | attributeName, tableName);
15 | }
16 |
17 | public AttributeValueDeserializationException(
18 | final String attributeName, final String tableName, final Throwable cause) {
19 | super(createMessage(attributeName, tableName), cause);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/exception/ColumnDeserializationException.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.exception;
6 |
7 | /** Exception thrown when a MySQL column value cannot be deserialized. */
8 | public final class ColumnDeserializationException extends EntityDeserializationException {
9 | private static final long serialVersionUID = 935990977706712032L;
10 |
11 | private static String createMessage(String columnName, String tableName) {
12 | return String.format(
13 | "Failed to deserialize MySQL column %s in table %s", columnName, tableName);
14 | }
15 |
16 | public ColumnDeserializationException(
17 | final String columnName, final String tableName, final Throwable cause) {
18 | super(createMessage(columnName, tableName), cause);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/exception/DestinationException.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.exception;
6 |
7 | public class DestinationException extends SpinaltapException {
8 | private static final long serialVersionUID = -2160287795842968357L;
9 |
10 | public DestinationException(String message, Throwable cause) {
11 | super(message, cause);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/exception/EntityDeserializationException.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.exception;
6 |
7 | public class EntityDeserializationException extends SpinaltapException {
8 | private static final long serialVersionUID = 2604256281318886726L;
9 |
10 | public EntityDeserializationException(String message, Throwable cause) {
11 | super(message, cause);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/exception/SourceException.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.exception;
6 |
7 | public class SourceException extends SpinaltapException {
8 | private static final long serialVersionUID = -59599391802331914L;
9 |
10 | public SourceException(String message, Throwable cause) {
11 | super(message, cause);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/exception/SpinaltapException.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.exception;
6 |
7 | public class SpinaltapException extends RuntimeException {
8 | private static final long serialVersionUID = -8074916613284028245L;
9 |
10 | public SpinaltapException(String message) {
11 | super(message);
12 | }
13 |
14 | public SpinaltapException(String message, Throwable cause) {
15 | super(message, cause);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/pipe/AbstractPipeFactory.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.pipe;
6 |
7 | import com.airbnb.common.metrics.TaggedMetricRegistry;
8 | import com.airbnb.spinaltap.common.config.SourceConfiguration;
9 | import com.airbnb.spinaltap.common.source.SourceState;
10 | import com.airbnb.spinaltap.common.util.StateRepositoryFactory;
11 | import java.net.InetAddress;
12 | import java.net.UnknownHostException;
13 | import java.util.List;
14 | import lombok.RequiredArgsConstructor;
15 | import lombok.extern.slf4j.Slf4j;
16 |
17 | @Slf4j
18 | @RequiredArgsConstructor
19 | public abstract class AbstractPipeFactory {
20 | private static String HOST_NAME = "unknown";
21 |
22 | protected final TaggedMetricRegistry metricRegistry;
23 |
24 | public abstract List createPipes(
25 | T sourceConfig,
26 | String partitionName,
27 | StateRepositoryFactory repositoryFactory,
28 | long leaderEpoch)
29 | throws Exception;
30 |
31 | protected static String getHostName() {
32 | if ("unknown".equalsIgnoreCase(HOST_NAME)) {
33 | try {
34 | HOST_NAME = InetAddress.getLocalHost().getCanonicalHostName();
35 | } catch (UnknownHostException e) {
36 | log.error("Could not retrieve host name", e);
37 | }
38 | }
39 |
40 | return HOST_NAME;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/pipe/PipeMetrics.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.pipe;
6 |
7 | import com.airbnb.common.metrics.TaggedMetricRegistry;
8 | import com.airbnb.spinaltap.common.metrics.SpinalTapMetrics;
9 | import com.google.common.collect.ImmutableMap;
10 |
11 | /** Responsible for metrics collection for a {@link Pipe}. */
12 | public class PipeMetrics extends SpinalTapMetrics {
13 | private static final String PIPE_PREFIX = METRIC_PREFIX + ".pipe";
14 |
15 | private static final String OPEN_METRIC = PIPE_PREFIX + ".open.count";
16 | private static final String CLOSE_METRIC = PIPE_PREFIX + ".close.count";
17 | private static final String START_METRIC = PIPE_PREFIX + ".start.count";
18 | private static final String STOP_METRIC = PIPE_PREFIX + ".stop.count";
19 | private static final String CHECKPOINT_METRIC = PIPE_PREFIX + ".checkpoint.count";
20 |
21 | public PipeMetrics(String sourceName, TaggedMetricRegistry metricRegistry) {
22 | this(ImmutableMap.of(SOURCE_NAME_TAG, sourceName), metricRegistry);
23 | }
24 |
25 | public PipeMetrics(ImmutableMap tags, TaggedMetricRegistry metricRegistry) {
26 | super(tags, metricRegistry);
27 | }
28 |
29 | public void open() {
30 | inc(OPEN_METRIC);
31 | }
32 |
33 | public void close() {
34 | inc(CLOSE_METRIC);
35 | }
36 |
37 | public void start() {
38 | inc(START_METRIC);
39 | }
40 |
41 | public void stop() {
42 | inc(STOP_METRIC);
43 | }
44 |
45 | public void checkpoint() {
46 | inc(CHECKPOINT_METRIC);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/source/AbstractDataStoreSource.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.source;
6 |
7 | import com.airbnb.spinaltap.Mutation;
8 | import com.airbnb.spinaltap.common.util.ConcurrencyUtil;
9 | import com.airbnb.spinaltap.common.util.Filter;
10 | import com.airbnb.spinaltap.common.util.Mapper;
11 | import com.google.common.util.concurrent.ThreadFactoryBuilder;
12 | import java.util.List;
13 | import java.util.concurrent.ExecutorService;
14 | import java.util.concurrent.Executors;
15 | import java.util.concurrent.TimeUnit;
16 | import javax.annotation.Nullable;
17 | import lombok.extern.slf4j.Slf4j;
18 |
19 | /**
20 | * Base implementation for Data Store source (Such as MySQL, DynamoDB).
21 | *
22 | * @param The event type produced by the source
23 | */
24 | @Slf4j
25 | public abstract class AbstractDataStoreSource extends AbstractSource {
26 | private @Nullable ExecutorService processor;
27 |
28 | public AbstractDataStoreSource(
29 | String name,
30 | SourceMetrics metrics,
31 | Mapper>> mutationMapper,
32 | Filter eventFilter) {
33 | super(name, metrics, mutationMapper, eventFilter);
34 | }
35 |
36 | @Override
37 | protected synchronized void start() {
38 | processor =
39 | Executors.newSingleThreadExecutor(
40 | new ThreadFactoryBuilder().setNameFormat(name + "-source-processor").build());
41 |
42 | processor.execute(
43 | () -> {
44 | try {
45 | connect();
46 | } catch (Exception ex) {
47 | started.set(false);
48 | metrics.startFailure(ex);
49 | log.error("Failed to stream events for source " + name, ex);
50 | }
51 | });
52 | }
53 |
54 | @Override
55 | protected void stop() throws Exception {
56 | if (isRunning()) {
57 | synchronized (this) {
58 | ConcurrencyUtil.shutdownGracefully(processor, 2, TimeUnit.SECONDS);
59 | }
60 | }
61 | disconnect();
62 | }
63 |
64 | @Override
65 | public synchronized boolean isStarted() {
66 | return started.get() && isRunning();
67 | }
68 |
69 | @Override
70 | protected synchronized boolean isRunning() {
71 | return processor != null && !processor.isShutdown();
72 | }
73 |
74 | @Override
75 | protected synchronized boolean isTerminated() {
76 | return processor == null || processor.isTerminated();
77 | }
78 |
79 | protected abstract void connect() throws Exception;
80 |
81 | protected abstract void disconnect() throws Exception;
82 |
83 | protected abstract boolean isConnected();
84 | }
85 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/source/ListenableSource.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.source;
6 |
7 | import com.airbnb.spinaltap.Mutation;
8 | import java.util.ArrayList;
9 | import java.util.List;
10 | import lombok.NonNull;
11 |
12 | /**
13 | * Base {@link Source} implement using observer pattern to allow listening to
15 | * streamed events and subscribe to lifecycle change notifications.
16 | */
17 | abstract class ListenableSource implements Source {
18 | private final List listeners = new ArrayList<>();
19 |
20 | @Override
21 | public void addListener(@NonNull final Listener listener) {
22 | listeners.add(listener);
23 | }
24 |
25 | @Override
26 | public void removeListener(@NonNull final Listener listener) {
27 | listeners.remove(listener);
28 | }
29 |
30 | protected void notifyMutations(final List extends Mutation>> mutations) {
31 | if (!mutations.isEmpty()) {
32 | listeners.forEach(listener -> listener.onMutation(mutations));
33 | }
34 | }
35 |
36 | protected void notifyEvent(E event) {
37 | listeners.forEach(listener -> listener.onEvent(event));
38 | }
39 |
40 | protected void notifyError(Throwable error) {
41 | listeners.forEach(listener -> listener.onError(error));
42 | }
43 |
44 | protected void notifyStart() {
45 | listeners.forEach(Source.Listener::onStart);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/source/MysqlSourceState.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.source;
6 |
7 | import com.airbnb.spinaltap.mysql.BinlogFilePos;
8 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
9 | import com.fasterxml.jackson.annotation.JsonProperty;
10 | import lombok.AllArgsConstructor;
11 | import lombok.Data;
12 | import lombok.EqualsAndHashCode;
13 | import lombok.NoArgsConstructor;
14 |
15 | /**
16 | * Represents the state of a {@link Source}, based on the last {@link SourceEvent} streamed. This is
17 | * used to mark the checkpoint for the {@link Source}, which will help indicate what position to
18 | * point to in the changelog on restart.
19 | *
20 | *
At the moment, the implement is coupled to binlog event state and therefore confined to {@code
21 | * MysqlSource} usage.
22 | */
23 | @Data
24 | @EqualsAndHashCode(callSuper = true)
25 | @NoArgsConstructor
26 | @AllArgsConstructor
27 | @JsonIgnoreProperties(ignoreUnknown = true)
28 | public class MysqlSourceState extends SourceState {
29 | /** The timestamp of the last streamed {@link SourceEvent} in the changelog. */
30 | @JsonProperty private long lastTimestamp;
31 |
32 | /** The offset of the last streamed {@link SourceEvent} in the changelog. */
33 | @JsonProperty private long lastOffset;
34 |
35 | /** The {@link BinlogFilePos} of the last streamed {@link SourceEvent} in the changelog. */
36 | @JsonProperty private BinlogFilePos lastPosition;
37 |
38 | public MysqlSourceState(
39 | final long lastTimestamp,
40 | final long lastOffset,
41 | final long currentLeaderEpoch,
42 | final BinlogFilePos lastPosition) {
43 | super(currentLeaderEpoch);
44 | this.lastTimestamp = lastTimestamp;
45 | this.lastOffset = lastOffset;
46 | this.lastPosition = lastPosition;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/source/Source.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.source;
6 |
7 | import com.airbnb.spinaltap.Mutation;
8 | import com.airbnb.spinaltap.common.destination.Destination;
9 | import java.util.List;
10 |
11 | /**
12 | * Represents the originating end of the {@link com.airbnb.spinaltap.common.pipe.Pipe}, where {@link
13 | * SourceEvent}s are received and transformed to {@link Mutation}s.
14 | */
15 | public interface Source {
16 | /** The name of the source */
17 | String getName();
18 |
19 | /** Adds a {@link Listener} to the source. */
20 | void addListener(Listener listener);
21 |
22 | /** Removes a {@link Listener} from the source. */
23 | void removeListener(Listener listener);
24 |
25 | /** Whether the source is started and processing events. */
26 | boolean isStarted();
27 |
28 | /**
29 | * Initializes the source and prepares for event processing.
30 | *
31 | *
The operation should be idempotent.
32 | */
33 | void open();
34 |
35 | /**
36 | * Stops event processing and closes the source.
37 | *
38 | *
The operation should be idempotent.
39 | */
40 | void close();
41 |
42 | /**
43 | * Clears the state of the source.
44 | *
45 | *
The operation should be idempotent.
46 | */
47 | void clear();
48 |
49 | /**
50 | * Commits the source checkpoint on the specified {@link Mutation}. On source start, streaming
51 | * will begin from the last marked checkpoint.
52 | */
53 | void checkpoint(Mutation> mutation);
54 |
55 | /** Represents a source listener to get notified of events and lifecycle changes. */
56 | abstract class Listener {
57 | /** Action to perform after the {@link Destination} has started. */
58 | public void onStart() {}
59 |
60 | /** Action to perform after a {@link SourceEvent}s has been received. */
61 | public void onEvent(SourceEvent event) {}
62 |
63 | /** Action to perform after a {@link Mutation}s has been detected. */
64 | public void onMutation(List extends Mutation>> mutations) {}
65 |
66 | /** Action to perform when an error is caught on processing an event. */
67 | public void onError(Throwable error) {}
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/source/SourceEvent.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.source;
6 |
7 | import lombok.AllArgsConstructor;
8 | import lombok.Getter;
9 | import lombok.NoArgsConstructor;
10 | import lombok.ToString;
11 |
12 | /** Represents a base event streamed from a {@link Source}. */
13 | @Getter
14 | @ToString
15 | @NoArgsConstructor
16 | @AllArgsConstructor
17 | public abstract class SourceEvent {
18 | private long timestamp = System.currentTimeMillis();
19 |
20 | /** Returns the number of entities in the event */
21 | public int size() {
22 | return 1;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/source/SourceState.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.source;
6 |
7 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
8 | import com.fasterxml.jackson.annotation.JsonProperty;
9 | import lombok.AllArgsConstructor;
10 | import lombok.Data;
11 | import lombok.NoArgsConstructor;
12 |
13 | @Data
14 | @NoArgsConstructor
15 | @AllArgsConstructor
16 | @JsonIgnoreProperties(ignoreUnknown = true)
17 | public abstract class SourceState {
18 | /**
19 | * The leader epoch for the {@code Source}. The epoch acts as a high watermark, and is typically
20 | * incremented on leader election.
21 | *
22 | *
Note: This is only applicable if a cluster solution is employed. It is is used to mitigate
23 | * network partition (split brain) scenarios, and avoid having two cluster nodes concurrently
24 | * streaming from the same {@link Source}.
25 | */
26 | @JsonProperty private long currentLeaderEpoch;
27 | }
28 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/util/BatchMapper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | import java.util.List;
8 |
9 | /**
10 | * Responsible for mapping a list of objects.
11 | *
12 | * @param The mapped from object type.
13 | * @param The mapped to object type.
14 | */
15 | @FunctionalInterface
16 | public interface BatchMapper {
17 | /**
18 | * Applies the mapping function on the list of objects.
19 | *
20 | * @param objects the objects to map.
21 | * @return the mapped objects.
22 | */
23 | List apply(List objects);
24 | }
25 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/util/ChainedFilter.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | import java.util.ArrayList;
8 | import java.util.List;
9 | import lombok.AccessLevel;
10 | import lombok.NoArgsConstructor;
11 | import lombok.NonNull;
12 | import lombok.RequiredArgsConstructor;
13 |
14 | /**
15 | * Represents a chain of {@link Filter}s, where all {@link Filter} conditions need to pass.
16 | *
17 | * @param the filtered object type.
18 | */
19 | @RequiredArgsConstructor
20 | public class ChainedFilter implements Filter {
21 | @NonNull private final List> filters;
22 |
23 | public static Builder builder() {
24 | return new Builder<>();
25 | }
26 |
27 | /**
28 | * Applies the filters on the object.
29 | *
30 | * @param object the object to filter.
31 | * @return {@code true} if all filter conditions pass, {@code false} otherwise.
32 | */
33 | public boolean apply(final T object) {
34 | return filters.stream().allMatch(filter -> filter.apply(object));
35 | }
36 |
37 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
38 | public static final class Builder {
39 | private final List> filters = new ArrayList<>();
40 |
41 | public Builder addFilter(Filter filter) {
42 | filters.add(filter);
43 | return this;
44 | }
45 |
46 | public Filter build() {
47 | return new ChainedFilter<>(filters);
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/util/ClassBasedMapper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | import com.google.common.base.Preconditions;
8 | import java.util.HashMap;
9 | import java.util.Map;
10 | import lombok.AccessLevel;
11 | import lombok.NoArgsConstructor;
12 | import lombok.NonNull;
13 | import lombok.RequiredArgsConstructor;
14 |
15 | /** Maps an object according to the registered mapper by {@link Class} type. */
16 | @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
17 | public class ClassBasedMapper implements Mapper {
18 | @NonNull private final Map, Mapper> locator;
19 |
20 | public static ClassBasedMapper.Builder builder() {
21 | return new ClassBasedMapper.Builder<>();
22 | }
23 |
24 | @Override
25 | public R map(@NonNull final T object) {
26 | Mapper mapper = locator.get(object.getClass());
27 | Preconditions.checkState(mapper != null, "No mapper found for type " + object.getClass());
28 |
29 | return mapper.map(object);
30 | }
31 |
32 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
33 | public static class Builder {
34 | private final Map, Mapper extends T, ? extends R>> locator =
35 | new HashMap<>();
36 |
37 | public Builder addMapper(Class klass, Mapper mapper) {
38 | locator.put(klass, mapper);
39 | return this;
40 | }
41 |
42 | @SuppressWarnings({"unchecked", "rawtypes"})
43 | public Mapper build() {
44 | return new ClassBasedMapper(locator);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/util/ConcurrencyUtil.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | import java.util.concurrent.ExecutorService;
8 | import java.util.concurrent.TimeUnit;
9 | import javax.validation.constraints.Min;
10 | import lombok.NonNull;
11 | import lombok.experimental.UtilityClass;
12 |
13 | /** Utility methods for concurrency operations */
14 | @UtilityClass
15 | public class ConcurrencyUtil {
16 | /**
17 | * Attempts to shutdown the {@link ExecutorService}. If the service does not terminate within the
18 | * specified timeout, a force shutdown will be triggered.
19 | *
20 | * @param executorService the {@link ExecutorService}.
21 | * @param timeout the timeout.
22 | * @param unit the time unit.
23 | * @return {@code true} if shutdown was successful within the specified timeout, {@code false}
24 | * otherwise.
25 | */
26 | public boolean shutdownGracefully(
27 | @NonNull ExecutorService executorService, @Min(1) long timeout, @NonNull TimeUnit unit) {
28 | boolean shutdown = false;
29 | executorService.shutdown();
30 | try {
31 | shutdown = executorService.awaitTermination(timeout, unit);
32 | } catch (InterruptedException e) {
33 | executorService.shutdownNow();
34 | }
35 | if (!shutdown) {
36 | executorService.shutdownNow();
37 | }
38 | return shutdown;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/util/ErrorHandler.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | import com.airbnb.spinaltap.common.exception.SpinaltapException;
8 |
9 | /** Responsible for handling {@code SpinaltapException}s. */
10 | @FunctionalInterface
11 | public interface ErrorHandler {
12 | void handle(SpinaltapException e) throws SpinaltapException;
13 | }
14 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/util/Filter.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | /**
8 | * Responsible for filtering object
9 | *
10 | * @param The filtered object type
11 | */
12 | @FunctionalInterface
13 | public interface Filter {
14 | /**
15 | * Applies the filter on the object
16 | *
17 | * @param object the object to filter
18 | * @return {@code true} if the filter condition passes, {@code false} otherwise
19 | */
20 | boolean apply(T object);
21 | }
22 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/util/Joiner.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | import javax.annotation.Nullable;
8 |
9 | /**
10 | * Joins two objects to produce a third.
11 | *
12 | * @param The first object type.
13 | * @param The second object type.
14 | * @param The result object type.
15 | */
16 | @FunctionalInterface
17 | public interface Joiner {
18 | /**
19 | * Applies the joiner on a pair of objects
20 | *
21 | * @param first the first object to join
22 | * @param second the second object to join
23 | * @return the resulting joined object
24 | */
25 | @Nullable
26 | R apply(@Nullable S first, @Nullable T second);
27 | }
28 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/util/JsonUtil.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | import com.fasterxml.jackson.databind.ObjectMapper;
8 | import com.fasterxml.jackson.datatype.joda.JodaModule;
9 | import lombok.experimental.UtilityClass;
10 |
11 | /** Utility class for json operations and components. */
12 | @UtilityClass
13 | public class JsonUtil {
14 | /** The {@link ObjectMapper} used for json SerDe. */
15 | public ObjectMapper OBJECT_MAPPER = new ObjectMapper();
16 |
17 | static {
18 | OBJECT_MAPPER.registerModule(new JodaModule());
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/util/KeyProvider.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | /**
8 | * Responsible for providing a key for an object.
9 | *
10 | * @param The object type.
11 | * @param The key type.
12 | */
13 | @FunctionalInterface
14 | public interface KeyProvider {
15 | /**
16 | * Gets the key for an object.
17 | *
18 | * @param object the object to get the key for.
19 | * @return the resulting key.
20 | */
21 | R get(T object);
22 | }
23 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/util/Mapper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | /**
8 | * Responsible for mapping between objects.
9 | *
10 | * @param The mapped from object type.
11 | * @param The mapped to object type.
12 | */
13 | @FunctionalInterface
14 | public interface Mapper {
15 | /**
16 | * Maps an object to another.
17 | *
18 | * @param object the object to map.
19 | * @return the resulting mapped object.
20 | */
21 | R map(T object);
22 | }
23 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/util/Repository.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | /**
8 | * Represents a single object data store repository.
9 | *
10 | * @param the object type stored in the repository.
11 | */
12 | public interface Repository {
13 | /** @return whether an object exists */
14 | boolean exists() throws Exception;
15 |
16 | /**
17 | * Creates a new object.
18 | *
19 | * @param value the object value.
20 | */
21 | void create(T value) throws Exception;
22 |
23 | /**
24 | * Sets the current object value.
25 | *
26 | * @param value the object value.
27 | */
28 | void set(T value) throws Exception;
29 |
30 | /**
31 | * Updates the current object value given a {@link DataUpdater}.
32 | *
33 | * @param value the object value.
34 | * @param updater the updater .
35 | */
36 | void update(T value, DataUpdater updater) throws Exception;
37 |
38 | /** Retrieves the current object value. */
39 | T get() throws Exception;
40 |
41 | /** Delete the current object */
42 | void remove() throws Exception;
43 |
44 | /**
45 | * Responsible for determining the object value, as a function of the current value and new value.
46 | *
47 | * @param the object value type.
48 | */
49 | interface DataUpdater {
50 | T apply(T currentValue, T newValue);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/util/StateRepositoryFactory.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | import com.airbnb.spinaltap.common.source.MysqlSourceState;
8 | import com.airbnb.spinaltap.common.source.SourceState;
9 | import java.util.Collection;
10 |
11 | /** Factory for {@link Repository}s that store {@link SourceState} objects. */
12 | public interface StateRepositoryFactory {
13 | /**
14 | * @return the {@link Repository} of the {@link MysqlSourceState} object for a given resource and
15 | * partition.
16 | */
17 | Repository getStateRepository(String resourceName, String partitionName);
18 |
19 | /**
20 | * @return the {@link Repository} of the history of {@link SourceState} objects for a given
21 | * resource and partition.
22 | */
23 | Repository> getStateHistoryRepository(String resourceName, String partitionName);
24 | }
25 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/util/ThreeWayJoiner.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | import javax.annotation.Nullable;
8 |
9 | /**
10 | * Joins three objects to produce a fourth.
11 | *
12 | * @param The first object type.
13 | * @param The second object type.
14 | * @param The third object type.
15 | * @param The result object type.
16 | */
17 | @FunctionalInterface
18 | public interface ThreeWayJoiner {
19 | /**
20 | * Applies the joiner on a triplet of objects.
21 | *
22 | * @param first the first object to join.
23 | * @param second the second object to join.
24 | * @param third the third object to join.
25 | * @return the resulting joined object.
26 | */
27 | @Nullable
28 | R apply(@Nullable X first, @Nullable Y second, @Nullable Z third);
29 | }
30 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/util/Validator.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | /**
8 | * Responsible for validation logic on an object.
9 | *
10 | * @param The object type.
11 | */
12 | public interface Validator {
13 | /**
14 | * Validates the object.
15 | *
16 | * @param object the object
17 | */
18 | void validate(T object);
19 |
20 | /** Resets the state of the validator */
21 | void reset();
22 | }
23 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/util/ZookeeperRepository.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | import com.fasterxml.jackson.core.type.TypeReference;
8 | import lombok.NonNull;
9 | import lombok.RequiredArgsConstructor;
10 | import org.apache.curator.framework.CuratorFramework;
11 |
12 | /**
13 | * {@link Repository} implement with Zookeeper as backing store for objects.
14 | *
15 | * @param the object type.
16 | */
17 | @RequiredArgsConstructor
18 | public class ZookeeperRepository implements Repository {
19 | @NonNull private final CuratorFramework zkClient;
20 | @NonNull private final String path;
21 | @NonNull private final TypeReference extends T> propertyClass;
22 |
23 | @Override
24 | public boolean exists() throws Exception {
25 | return zkClient.checkExists().forPath(path) != null;
26 | }
27 |
28 | @Override
29 | public void create(T data) throws Exception {
30 | zkClient
31 | .create()
32 | .creatingParentsIfNeeded()
33 | .forPath(path, JsonUtil.OBJECT_MAPPER.writeValueAsBytes(data));
34 | }
35 |
36 | @Override
37 | public void set(T data) throws Exception {
38 | zkClient.setData().forPath(path, JsonUtil.OBJECT_MAPPER.writeValueAsBytes(data));
39 | }
40 |
41 | @Override
42 | public void update(T data, DataUpdater updater) throws Exception {
43 | if (exists()) {
44 | set(updater.apply(get(), data));
45 | } else {
46 | create(data);
47 | }
48 | }
49 |
50 | @Override
51 | public T get() throws Exception {
52 | byte[] value = zkClient.getData().forPath(path);
53 | return JsonUtil.OBJECT_MAPPER.readValue(value, propertyClass);
54 | }
55 |
56 | @Override
57 | public void remove() throws Exception {
58 | if (exists()) {
59 | zkClient.delete().guaranteed().forPath(path);
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/spinaltap-common/src/main/java/com/airbnb/spinaltap/common/validator/MutationOrderValidator.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.validator;
6 |
7 | import com.airbnb.spinaltap.Mutation;
8 | import com.airbnb.spinaltap.common.util.Validator;
9 | import java.util.concurrent.atomic.AtomicLong;
10 | import java.util.function.Consumer;
11 | import lombok.NonNull;
12 | import lombok.RequiredArgsConstructor;
13 | import lombok.extern.slf4j.Slf4j;
14 |
15 | /**
16 | * Responsible for validating that {@link Mutation}s are streamed in order. This is typically used
17 | * as a {@link com.airbnb.spinaltap.common.source.Source.Listener} or {@link
18 | * com.airbnb.spinaltap.common.destination.Destination.Listener} to enforce {@link Mutation}
19 | * ordering guarantee in run-time.
20 | */
21 | @Slf4j
22 | @RequiredArgsConstructor
23 | public final class MutationOrderValidator implements Validator> {
24 | /** The handler to execute on out-of-order {@link Mutation}. */
25 | @NonNull private final Consumer> handler;
26 |
27 | /** The id of the last {@link Mutation} validated so far. */
28 | private AtomicLong lastSeenId = new AtomicLong(-1);
29 |
30 | /**
31 | * Validates a {@link Mutation} is received in order, otherwise triggers the specified handler.
32 | */
33 | @Override
34 | public void validate(@NonNull final Mutation> mutation) {
35 | final long mutationId = mutation.getMetadata().getId();
36 | log.debug("Validating order for mutation with id {}.", mutationId);
37 |
38 | if (lastSeenId.get() > mutationId) {
39 | log.warn(
40 | "Mutation with id {} is out of order and should precede {}.", mutationId, lastSeenId);
41 | handler.accept(mutation);
42 | }
43 |
44 | lastSeenId.set(mutationId);
45 | }
46 |
47 | /** Resets the state of the {@link Validator}. */
48 | @Override
49 | public void reset() {
50 | lastSeenId.set(-1);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/spinaltap-common/src/test/java/com/airbnb/spinaltap/common/destination/BufferedDestinationTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.destination;
6 |
7 | import static org.junit.Assert.assertTrue;
8 | import static org.mockito.Mockito.mock;
9 | import static org.mockito.Mockito.verify;
10 | import static org.mockito.Mockito.when;
11 |
12 | import com.airbnb.spinaltap.Mutation;
13 | import com.google.common.collect.ImmutableList;
14 | import java.util.List;
15 | import org.junit.Before;
16 | import org.junit.Test;
17 |
18 | public class BufferedDestinationTest {
19 | private final Mutation> firstMutation = mock(Mutation.class);
20 | private final Mutation> secondMutation = mock(Mutation.class);
21 | private final Mutation> thirdMutation = mock(Mutation.class);
22 |
23 | private final List> mutations =
24 | ImmutableList.of(firstMutation, secondMutation, thirdMutation);
25 |
26 | private final Destination destination = mock(Destination.class);
27 | private final Destination.Listener listener = mock(Destination.Listener.class);
28 | private final DestinationMetrics metrics = mock(DestinationMetrics.class);
29 |
30 | private BufferedDestination bufferedDestination =
31 | new BufferedDestination("test", 10, destination, metrics);
32 |
33 | @Before
34 | public void setUp() throws Exception {
35 | bufferedDestination.addListener(listener);
36 | }
37 |
38 | @Test
39 | public void testOpenClose() throws Exception {
40 | when(destination.isStarted()).thenReturn(false);
41 |
42 | bufferedDestination.open();
43 |
44 | when(destination.isStarted()).thenReturn(true);
45 |
46 | assertTrue(bufferedDestination.isStarted());
47 | verify(destination).open();
48 |
49 | bufferedDestination.open();
50 |
51 | assertTrue(bufferedDestination.isStarted());
52 | verify(destination).open();
53 |
54 | bufferedDestination.close();
55 |
56 | verify(destination).close();
57 | }
58 |
59 | @Test
60 | public void testSend() throws Exception {
61 | when(destination.isStarted()).thenReturn(true);
62 |
63 | bufferedDestination.send(ImmutableList.of(firstMutation, secondMutation));
64 | bufferedDestination.processMutations();
65 |
66 | verify(destination).send(ImmutableList.of(firstMutation, secondMutation));
67 |
68 | bufferedDestination.send(ImmutableList.of(firstMutation));
69 | bufferedDestination.send(ImmutableList.of(secondMutation, thirdMutation));
70 | bufferedDestination.processMutations();
71 |
72 | verify(destination).send(mutations);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/spinaltap-common/src/test/java/com/airbnb/spinaltap/common/destination/DestinationBuilderTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.destination;
6 |
7 | import static org.junit.Assert.assertEquals;
8 | import static org.junit.Assert.assertTrue;
9 | import static org.mockito.Mockito.mock;
10 |
11 | import com.airbnb.common.metrics.TaggedMetricRegistry;
12 | import com.airbnb.spinaltap.Mutation;
13 | import com.airbnb.spinaltap.common.util.KeyProvider;
14 | import com.airbnb.spinaltap.common.util.Mapper;
15 | import lombok.NoArgsConstructor;
16 | import org.junit.Test;
17 |
18 | public class DestinationBuilderTest {
19 | private static final Mapper, Mutation>> mapper = mutation -> mutation;
20 | private static final DestinationMetrics metrics =
21 | new DestinationMetrics("test", "test", new TaggedMetricRegistry());
22 |
23 | @Test(expected = NullPointerException.class)
24 | public void testNoMapper() throws Exception {
25 | new TestDestinationBuilder().withMetrics(metrics).build();
26 | }
27 |
28 | @Test(expected = NullPointerException.class)
29 | public void testNoMetrics() throws Exception {
30 | new TestDestinationBuilder().withMapper(mapper).build();
31 | }
32 |
33 | @Test
34 | public void testBuildBufferedDestination() throws Exception {
35 | Destination destination =
36 | new TestDestinationBuilder().withMapper(mapper).withMetrics(metrics).withBuffer(5).build();
37 |
38 | assertTrue(destination instanceof BufferedDestination);
39 | assertEquals(5, ((BufferedDestination) destination).getRemainingCapacity());
40 | }
41 |
42 | @Test
43 | public void testBuildDestinationPool() throws Exception {
44 | Destination destination =
45 | new TestDestinationBuilder()
46 | .withMapper(mapper)
47 | .withMetrics(metrics)
48 | .withBuffer(5)
49 | .withPool(7, mock(KeyProvider.class))
50 | .build();
51 |
52 | assertTrue(destination instanceof DestinationPool);
53 | assertEquals(7, ((DestinationPool) destination).getPoolSize());
54 | }
55 |
56 | @NoArgsConstructor
57 | class TestDestinationBuilder extends DestinationBuilder> {
58 | @Override
59 | protected Destination createDestination() {
60 | return mock(Destination.class);
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/spinaltap-common/src/test/java/com/airbnb/spinaltap/common/destination/ListenableDestinationTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.destination;
6 |
7 | import static org.mockito.Mockito.mock;
8 | import static org.mockito.Mockito.verify;
9 | import static org.mockito.Mockito.verifyNoMoreInteractions;
10 |
11 | import com.airbnb.spinaltap.Mutation;
12 | import com.google.common.collect.ImmutableList;
13 | import java.util.List;
14 | import org.junit.Test;
15 |
16 | public class ListenableDestinationTest {
17 | private final Destination.Listener listener = mock(Destination.Listener.class);
18 |
19 | private ListenableDestination destination = new TestListenableDestination();
20 |
21 | @Test
22 | public void test() throws Exception {
23 | Exception exception = mock(Exception.class);
24 | List> mutations = ImmutableList.of(mock(Mutation.class));
25 |
26 | destination.addListener(listener);
27 |
28 | destination.notifyStart();
29 | destination.notifySend(mutations);
30 | destination.notifyError(exception);
31 |
32 | verify(listener).onStart();
33 | verify(listener).onSend(mutations);
34 | verify(listener).onError(exception);
35 |
36 | destination.removeListener(listener);
37 |
38 | destination.notifyStart();
39 | destination.notifySend(mutations);
40 | destination.notifyError(exception);
41 |
42 | verifyNoMoreInteractions(listener);
43 | }
44 |
45 | private static final class TestListenableDestination extends ListenableDestination {
46 | @Override
47 | public Mutation> getLastPublishedMutation() {
48 | return null;
49 | }
50 |
51 | @Override
52 | public void send(List extends Mutation>> mutations) {}
53 |
54 | @Override
55 | public boolean isStarted() {
56 | return false;
57 | }
58 |
59 | @Override
60 | public void close() {}
61 |
62 | @Override
63 | public void clear() {}
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/spinaltap-common/src/test/java/com/airbnb/spinaltap/common/pipe/PipeManagerTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.pipe;
6 |
7 | import static org.junit.Assert.*;
8 | import static org.mockito.Mockito.*;
9 |
10 | import com.google.common.collect.ImmutableList;
11 | import org.junit.Test;
12 |
13 | public class PipeManagerTest {
14 | private static final String NAME = "test";
15 | private static final String PARTITION = "test_0";
16 |
17 | private final Pipe firstPipe = mock(Pipe.class);
18 | private final Pipe secondPipe = mock(Pipe.class);
19 |
20 | @Test
21 | public void testAddRemovePipe() throws Exception {
22 | PipeManager pipeManager = new PipeManager();
23 |
24 | pipeManager.addPipes(NAME, PARTITION, ImmutableList.of(firstPipe, secondPipe));
25 |
26 | verify(firstPipe, times(1)).start();
27 | verify(secondPipe, times(1)).start();
28 |
29 | pipeManager.removePipe(NAME, PARTITION);
30 |
31 | verify(firstPipe, times(1)).stop();
32 | verify(secondPipe, times(1)).stop();
33 |
34 | assertTrue(pipeManager.isEmpty());
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/spinaltap-common/src/test/java/com/airbnb/spinaltap/common/pipe/PipeTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.pipe;
6 |
7 | import static org.junit.Assert.*;
8 | import static org.mockito.Mockito.*;
9 |
10 | import com.airbnb.spinaltap.Mutation;
11 | import com.airbnb.spinaltap.common.destination.Destination;
12 | import com.airbnb.spinaltap.common.source.Source;
13 | import org.junit.Before;
14 | import org.junit.Test;
15 |
16 | public class PipeTest {
17 | private final Source source = mock(Source.class);
18 | private final Destination destination = mock(Destination.class);
19 | private final PipeMetrics metrics = mock(PipeMetrics.class);
20 | private final Mutation lastMutation = mock(Mutation.class);
21 |
22 | private final Pipe pipe = new Pipe(source, destination, metrics);
23 |
24 | @Before
25 | public void setUp() throws Exception {
26 | when(destination.getLastPublishedMutation()).thenReturn(lastMutation);
27 | }
28 |
29 | @Test
30 | public void testStartStop() throws Exception {
31 | Mutation mutation = mock(Mutation.class);
32 | Mutation.Metadata metadata = mock(Mutation.Metadata.class);
33 |
34 | when(destination.getLastPublishedMutation()).thenReturn(mutation);
35 | when(mutation.getMetadata()).thenReturn(metadata);
36 |
37 | pipe.start();
38 |
39 | when(source.isStarted()).thenReturn(true);
40 | when(destination.isStarted()).thenReturn(true);
41 |
42 | verify(source, times(1)).addListener(any(Source.Listener.class));
43 | verify(source, times(1)).open();
44 |
45 | verify(destination, times(1)).addListener(any(Destination.Listener.class));
46 | verify(destination, times(1)).open();
47 |
48 | verify(metrics, times(1)).open();
49 |
50 | pipe.stop();
51 |
52 | verify(source, times(1)).removeListener(any(Source.Listener.class));
53 | verify(source, times(1)).checkpoint(mutation);
54 | verify(source, times(1)).close();
55 |
56 | verify(destination, times(1)).removeListener(any(Destination.Listener.class));
57 | verify(destination, times(1)).close();
58 |
59 | verify(metrics, times(1)).close();
60 | }
61 |
62 | @Test
63 | public void testIsStarted() throws Exception {
64 | when(source.isStarted()).thenReturn(true);
65 | when(destination.isStarted()).thenReturn(false);
66 |
67 | assertFalse(pipe.isStarted());
68 |
69 | when(source.isStarted()).thenReturn(false);
70 | when(destination.isStarted()).thenReturn(true);
71 |
72 | assertFalse(pipe.isStarted());
73 |
74 | when(source.isStarted()).thenReturn(true);
75 | when(destination.isStarted()).thenReturn(true);
76 |
77 | assertTrue(pipe.isStarted());
78 | }
79 |
80 | @Test
81 | public void testCheckpoint() throws Exception {
82 | pipe.checkpoint();
83 |
84 | verify(source, times(1)).checkpoint(lastMutation);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/spinaltap-common/src/test/java/com/airbnb/spinaltap/common/source/ListenableSourceTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.source;
6 |
7 | import static org.mockito.Mockito.mock;
8 | import static org.mockito.Mockito.verify;
9 | import static org.mockito.Mockito.verifyNoMoreInteractions;
10 |
11 | import com.airbnb.spinaltap.Mutation;
12 | import org.junit.Test;
13 |
14 | public class ListenableSourceTest {
15 | private final Source.Listener listener = mock(Source.Listener.class);
16 |
17 | private ListenableSource source = new TestListenableSource();
18 |
19 | @Test
20 | public void test() throws Exception {
21 | Exception exception = mock(Exception.class);
22 | SourceEvent event = mock(SourceEvent.class);
23 |
24 | source.addListener(listener);
25 |
26 | source.notifyStart();
27 | source.notifyEvent(event);
28 | source.notifyError(exception);
29 |
30 | verify(listener).onStart();
31 | verify(listener).onEvent(event);
32 | verify(listener).onError(exception);
33 |
34 | source.removeListener(listener);
35 |
36 | source.notifyStart();
37 | source.notifyEvent(event);
38 | source.notifyError(exception);
39 |
40 | verifyNoMoreInteractions(listener);
41 | }
42 |
43 | private static final class TestListenableSource extends ListenableSource {
44 | @Override
45 | public String getName() {
46 | return null;
47 | }
48 |
49 | @Override
50 | public boolean isStarted() {
51 | return false;
52 | }
53 |
54 | @Override
55 | public void open() {}
56 |
57 | @Override
58 | public void close() {}
59 |
60 | @Override
61 | public void clear() {}
62 |
63 | @Override
64 | public void checkpoint(Mutation> mutation) {}
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/spinaltap-common/src/test/java/com/airbnb/spinaltap/common/util/ChainedFilterTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | import static org.junit.Assert.assertFalse;
8 | import static org.junit.Assert.assertTrue;
9 |
10 | import org.junit.Test;
11 |
12 | public class ChainedFilterTest {
13 | @Test
14 | public void testFailingFilter() throws Exception {
15 | Filter filter =
16 | ChainedFilter.builder().addFilter(num -> true).addFilter(num -> false).build();
17 |
18 | assertFalse(filter.apply(1));
19 | }
20 |
21 | @Test
22 | public void testPassingFilter() throws Exception {
23 | Filter filter =
24 | ChainedFilter.builder().addFilter(num -> true).addFilter(num -> true).build();
25 |
26 | assertTrue(filter.apply(1));
27 | }
28 |
29 | @Test
30 | public void testEmptyFilter() throws Exception {
31 | Filter filter = ChainedFilter.builder().build();
32 |
33 | assertTrue(filter.apply(1));
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/spinaltap-common/src/test/java/com/airbnb/spinaltap/common/util/ClassBasedMapperTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Airbnb. Licensed under Apache-2.0. See License in the project root for license
3 | * information.
4 | */
5 | package com.airbnb.spinaltap.common.util;
6 |
7 | import static org.junit.Assert.assertEquals;
8 |
9 | import org.junit.Test;
10 |
11 | public class ClassBasedMapperTest {
12 | @Test
13 | public void testMap() throws Exception {
14 | Mapper