GH-2311: Migrate AWS Kinesis binder to core
Fixes https://github.com/spring-cloud/spring-cloud-stream/issues/2311 * Use Tescontainers for Localstack * Fix tests according new `core` status quo * Improve `KinesisStreamProvisioner` to wait for stream become active before moving on to other logic Disable Kinesis binder LocalstackContainerTest on mac
This commit is contained in:
29
binders/kinesis-binder/.gitignore
vendored
Normal file
29
binders/kinesis-binder/.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
apps/
|
||||
/application.yml
|
||||
/application.properties
|
||||
asciidoctor.css
|
||||
*~
|
||||
.#*
|
||||
*#
|
||||
target/
|
||||
build/
|
||||
bin/
|
||||
_site/
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.DS_Store
|
||||
*.sw*
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
.factorypath
|
||||
spring-xd-samples/*/xd
|
||||
dump.rdb
|
||||
coverage-error.log
|
||||
.apt_generated
|
||||
aws.credentials.properties
|
||||
/spring-cloud-stream-binder-kinesis/nbproject/
|
||||
nb-configuration.xml
|
||||
1
binders/kinesis-binder/.mvn/maven.config
Normal file
1
binders/kinesis-binder/.mvn/maven.config
Normal file
@@ -0,0 +1 @@
|
||||
-DaltSnapshotDeploymentRepository=repo.spring.io::default::https://repo.spring.io/libs-snapshot-local -P spring
|
||||
117
binders/kinesis-binder/.mvn/wrapper/MavenWrapperDownloader.java
vendored
Normal file
117
binders/kinesis-binder/.mvn/wrapper/MavenWrapperDownloader.java
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2007-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import java.net.*;
|
||||
import java.io.*;
|
||||
import java.nio.channels.*;
|
||||
import java.util.Properties;
|
||||
|
||||
public class MavenWrapperDownloader {
|
||||
|
||||
private static final String WRAPPER_VERSION = "0.5.6";
|
||||
/**
|
||||
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
|
||||
*/
|
||||
private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
|
||||
+ WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
|
||||
|
||||
/**
|
||||
* Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
|
||||
* use instead of the default one.
|
||||
*/
|
||||
private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
|
||||
".mvn/wrapper/maven-wrapper.properties";
|
||||
|
||||
/**
|
||||
* Path where the maven-wrapper.jar will be saved to.
|
||||
*/
|
||||
private static final String MAVEN_WRAPPER_JAR_PATH =
|
||||
".mvn/wrapper/maven-wrapper.jar";
|
||||
|
||||
/**
|
||||
* Name of the property which should be used to override the default download url for the wrapper.
|
||||
*/
|
||||
private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
|
||||
|
||||
public static void main(String args[]) {
|
||||
System.out.println("- Downloader started");
|
||||
File baseDirectory = new File(args[0]);
|
||||
System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
|
||||
|
||||
// If the maven-wrapper.properties exists, read it and check if it contains a custom
|
||||
// wrapperUrl parameter.
|
||||
File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
|
||||
String url = DEFAULT_DOWNLOAD_URL;
|
||||
if(mavenWrapperPropertyFile.exists()) {
|
||||
FileInputStream mavenWrapperPropertyFileInputStream = null;
|
||||
try {
|
||||
mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
|
||||
Properties mavenWrapperProperties = new Properties();
|
||||
mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
|
||||
url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
|
||||
} catch (IOException e) {
|
||||
System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
|
||||
} finally {
|
||||
try {
|
||||
if(mavenWrapperPropertyFileInputStream != null) {
|
||||
mavenWrapperPropertyFileInputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore ...
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.println("- Downloading from: " + url);
|
||||
|
||||
File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
|
||||
if(!outputFile.getParentFile().exists()) {
|
||||
if(!outputFile.getParentFile().mkdirs()) {
|
||||
System.out.println(
|
||||
"- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
|
||||
}
|
||||
}
|
||||
System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
|
||||
try {
|
||||
downloadFileFromURL(url, outputFile);
|
||||
System.out.println("Done");
|
||||
System.exit(0);
|
||||
} catch (Throwable e) {
|
||||
System.out.println("- Error downloading");
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void downloadFileFromURL(String urlString, File destination) throws Exception {
|
||||
if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
|
||||
String username = System.getenv("MVNW_USERNAME");
|
||||
char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
|
||||
Authenticator.setDefault(new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
return new PasswordAuthentication(username, password);
|
||||
}
|
||||
});
|
||||
}
|
||||
URL website = new URL(urlString);
|
||||
ReadableByteChannel rbc;
|
||||
rbc = Channels.newChannel(website.openStream());
|
||||
FileOutputStream fos = new FileOutputStream(destination);
|
||||
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
|
||||
fos.close();
|
||||
rbc.close();
|
||||
}
|
||||
|
||||
}
|
||||
BIN
binders/kinesis-binder/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
BIN
binders/kinesis-binder/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
Binary file not shown.
2
binders/kinesis-binder/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
2
binders/kinesis-binder/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
|
||||
66
binders/kinesis-binder/.settings.xml
Normal file
66
binders/kinesis-binder/.settings.xml
Normal file
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<settings>
|
||||
<servers>
|
||||
<server>
|
||||
<id>repo.spring.io</id>
|
||||
<username>${env.CI_DEPLOY_USERNAME}</username>
|
||||
<password>${env.CI_DEPLOY_PASSWORD}</password>
|
||||
</server>
|
||||
</servers>
|
||||
<profiles>
|
||||
<profile>
|
||||
<!--
|
||||
N.B. this profile is only here to support users and IDEs that do not use Maven 3.3.
|
||||
It isn't needed on the command line if you use the wrapper script (mvnw) or if you use
|
||||
a native Maven with the right version. Eclipse users should points their Maven tooling to
|
||||
this settings file, or copy the profile into their ~/.m2/settings.xml.
|
||||
-->
|
||||
<id>spring</id>
|
||||
<activation><activeByDefault>true</activeByDefault></activation>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/libs-snapshot-local</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/libs-milestone-local</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-releases</id>
|
||||
<name>Spring Releases</name>
|
||||
<url>https://repo.spring.io/release</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/libs-snapshot-local</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/libs-milestone-local</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
</settings>
|
||||
201
binders/kinesis-binder/LICENSE
Normal file
201
binders/kinesis-binder/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
245
binders/kinesis-binder/mvnw
vendored
Normal file
245
binders/kinesis-binder/mvnw
vendored
Normal file
@@ -0,0 +1,245 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Maven2 Start Up Batch script
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
# JAVA_HOME - location of a JDK home dir
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# M2_HOME - location of maven2's installed home dir
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||
|
||||
if [ -f /etc/mavenrc ] ; then
|
||||
. /etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.mavenrc" ] ; then
|
||||
. "$HOME/.mavenrc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# OS specific support. $var _must_ be set to either true or false.
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "`uname`" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
#
|
||||
# Look for the Apple JDKs first to preserve the existing behaviour, and then look
|
||||
# for the new JDKs provided by Oracle.
|
||||
#
|
||||
if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then
|
||||
#
|
||||
# Apple JDKs
|
||||
#
|
||||
export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then
|
||||
#
|
||||
# Apple JDKs
|
||||
#
|
||||
export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then
|
||||
#
|
||||
# Oracle JDKs
|
||||
#
|
||||
export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then
|
||||
#
|
||||
# Apple JDKs
|
||||
#
|
||||
export JAVA_HOME=`/usr/libexec/java_home`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=`java-config --jre-home`
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$M2_HOME" ] ; then
|
||||
## resolve links - $0 may be a link to maven's home
|
||||
PRG="$0"
|
||||
|
||||
# need this for relative symlinks
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG="`dirname "$PRG"`/$link"
|
||||
fi
|
||||
done
|
||||
|
||||
saveddir=`pwd`
|
||||
|
||||
M2_HOME=`dirname "$PRG"`/..
|
||||
|
||||
# make it fully qualified
|
||||
M2_HOME=`cd "$M2_HOME" && pwd`
|
||||
|
||||
cd "$saveddir"
|
||||
# echo Using m2 at $M2_HOME
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --unix "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
|
||||
fi
|
||||
|
||||
# For Migwn, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME="`(cd "$M2_HOME"; pwd)`"
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
|
||||
# TODO classpath?
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="`which javac`"
|
||||
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=`which readlink`
|
||||
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
|
||||
else
|
||||
javaExecutable="`readlink -f \"$javaExecutable\"`"
|
||||
fi
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVACMD" ] ; then
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="`which java`"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||
echo " We cannot execute $JAVACMD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --path --windows "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
|
||||
fi
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
local basedir=$(pwd)
|
||||
local wdir=$(pwd)
|
||||
while [ "$wdir" != '/' ] ; do
|
||||
if [ -d "$wdir"/.mvn ] ; then
|
||||
basedir=$wdir
|
||||
break
|
||||
fi
|
||||
wdir=$(cd "$wdir/.."; pwd)
|
||||
done
|
||||
echo "${basedir}"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
echo "$(tr -s '\n' ' ' < "$1")"
|
||||
fi
|
||||
}
|
||||
|
||||
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)}
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
# work with both Windows and non-Windows executions.
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
|
||||
export MAVEN_CMD_LINE_ARGS
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
echo "Running version check"
|
||||
VERSION=$( sed '\!<parent!,\!</parent!d' `dirname $0`/pom.xml | grep '<version' | head -1 | sed -e 's/.*<version>//' -e 's!</version>.*$!!' )
|
||||
echo "The found version is [${VERSION}]"
|
||||
|
||||
if echo $VERSION | egrep -q 'M|RC'; then
|
||||
echo Activating \"milestone\" profile for version=\"$VERSION\"
|
||||
echo $MAVEN_ARGS | grep -q milestone || MAVEN_ARGS="$MAVEN_ARGS -Pmilestone"
|
||||
else
|
||||
echo Deactivating \"milestone\" profile for version=\"$VERSION\"
|
||||
echo $MAVEN_ARGS | grep -q milestone && MAVEN_ARGS=$(echo $MAVEN_ARGS | sed -e 's/-Pmilestone//')
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} ${MAVEN_ARGS} "$@"
|
||||
143
binders/kinesis-binder/mvnw.cmd
vendored
Normal file
143
binders/kinesis-binder/mvnw.cmd
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Maven2 Start Up Batch script
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM M2_HOME - location of maven2's installed home dir
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@REM e.g. to debug Maven itself, use
|
||||
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||
@echo off
|
||||
@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
|
||||
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||
|
||||
@REM set %HOME% to equivalent of $HOME
|
||||
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||
|
||||
@REM Execute a user defined script before this one
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
|
||||
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
|
||||
:skipRcPre
|
||||
|
||||
@setlocal
|
||||
|
||||
set ERROR_CODE=0
|
||||
|
||||
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||
@setlocal
|
||||
|
||||
@REM ==== START VALIDATION ====
|
||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME not found in your environment. >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
:OkJHome
|
||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
@REM ==== END VALIDATION ====
|
||||
|
||||
:init
|
||||
|
||||
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||
@REM Fallback to current working directory if not found.
|
||||
|
||||
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||
|
||||
set EXEC_DIR=%CD%
|
||||
set WDIR=%EXEC_DIR%
|
||||
:findBaseDir
|
||||
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||
cd ..
|
||||
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||
set WDIR=%CD%
|
||||
goto findBaseDir
|
||||
|
||||
:baseDirFound
|
||||
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||
cd "%EXEC_DIR%"
|
||||
goto endDetectBaseDir
|
||||
|
||||
:baseDirNotFound
|
||||
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||
cd "%EXEC_DIR%"
|
||||
|
||||
:endDetectBaseDir
|
||||
|
||||
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||
|
||||
@setlocal EnableExtensions EnableDelayedExpansion
|
||||
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||
|
||||
:endReadAdditionalConfig
|
||||
|
||||
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||
if ERRORLEVEL 1 goto error
|
||||
goto end
|
||||
|
||||
:error
|
||||
set ERROR_CODE=1
|
||||
|
||||
:end
|
||||
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
|
||||
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
|
||||
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
|
||||
:skipRcPost
|
||||
|
||||
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||
if "%MAVEN_BATCH_PAUSE%" == "on" pause
|
||||
|
||||
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
|
||||
|
||||
exit /B %ERROR_CODE%
|
||||
244
binders/kinesis-binder/pom.xml
Normal file
244
binders/kinesis-binder/pom.xml
Normal file
@@ -0,0 +1,244 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-build</artifactId>
|
||||
<version>4.0.0-SNAPSHOT</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<artifactId>spring-cloud-stream-binder-kinesis-parent</artifactId>
|
||||
<version>4.0.0-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<properties>
|
||||
<spring-cloud-stream.version>4.0.0-SNAPSHOT</spring-cloud-stream.version>
|
||||
<java.version>17</java.version>
|
||||
<spring-cloud-aws.version>2.4.0</spring-cloud-aws.version>
|
||||
<spring-integration-aws.version>3.0.0-SNAPSHOT</spring-integration-aws.version>
|
||||
<dynamodb-lock-client.version>1.1.0</dynamodb-lock-client.version>
|
||||
<amazon-kinesis-client.version>1.14.8</amazon-kinesis-client.version>
|
||||
<amazon-kinesis-producer.version>0.14.12</amazon-kinesis-producer.version>
|
||||
<dynamodb-stream.version>1.5.3</dynamodb-stream.version>
|
||||
<testcontainers.version>1.16.3</testcontainers.version>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
<module>spring-cloud-stream-binder-kinesis</module>
|
||||
<module>spring-cloud-stream-binder-kinesis-docs</module>
|
||||
</modules>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-binder-kinesis</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream</artifactId>
|
||||
<version>${spring-cloud-stream.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.integration</groupId>
|
||||
<artifactId>spring-integration-aws</artifactId>
|
||||
<version>${spring-integration-aws.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>dynamodb-lock-client</artifactId>
|
||||
<version>${dynamodb-lock-client.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>dynamodb-streams-kinesis-adapter</artifactId>
|
||||
<version>${dynamodb-stream.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>amazon-kinesis-client</artifactId>
|
||||
<version>${amazon-kinesis-client.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>amazon-kinesis-producer</artifactId>
|
||||
<version>${amazon-kinesis-producer.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-binder-test</artifactId>
|
||||
<version>${spring-cloud-stream.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-test-support-internal</artifactId>
|
||||
<version>${spring-cloud-stream.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.awspring.cloud</groupId>
|
||||
<artifactId>spring-cloud-aws-dependencies</artifactId>
|
||||
<version>${spring-cloud-aws.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers-bom</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<configuration>
|
||||
<quiet>true</quiet>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
<compilerArgument>-parameters</compilerArgument>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<reporting>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</reporting>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>spring</id>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-releases</id>
|
||||
<name>Spring Releases</name>
|
||||
<url>https://repo.spring.io/release</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>spring-releases</id>
|
||||
<name>Spring Releases</name>
|
||||
<url>https://repo.spring.io/release</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-releases</id>
|
||||
<name>Spring Releases</name>
|
||||
<url>https://repo.spring.io/release</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>spring-releases</id>
|
||||
<name>Spring Releases</name>
|
||||
<url>https://repo.spring.io/release</url>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
</project>
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>spring-cloud-stream-binder-kinesis-docs</artifactId>
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-binder-kinesis-parent</artifactId>
|
||||
<version>4.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<packaging>jar</packaging>
|
||||
<name>spring-cloud-stream-binder-kinesis-docs</name>
|
||||
<description>Spring Cloud Stream AWS Kinesis Binder Docs</description>
|
||||
<properties>
|
||||
<docs.main>spring-cloud-stream-binder-kinesis</docs.main>
|
||||
<main.basedir>${basedir}/..</main.basedir>
|
||||
<maven.plugin.plugin.version>3.4</maven.plugin.plugin.version>
|
||||
<configprops.inclusionPattern>.*stream.*</configprops.inclusionPattern>
|
||||
<upload-docs-zip.phase>deploy</upload-docs-zip.phase>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>spring-cloud-stream-binder-kinesis</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<sourceDirectory>src/main/asciidoc</sourceDirectory>
|
||||
</build>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>docs</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>pl.project13.maven</groupId>
|
||||
<artifactId>git-commit-id-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.asciidoctor</groupId>
|
||||
<artifactId>asciidoctor-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
@@ -0,0 +1,20 @@
|
||||
require 'asciidoctor'
|
||||
require 'erb'
|
||||
|
||||
guard 'shell' do
|
||||
watch(/.*\.adoc$/) {|m|
|
||||
Asciidoctor.render_file('index.adoc', \
|
||||
:in_place => true, \
|
||||
:safe => Asciidoctor::SafeMode::UNSAFE, \
|
||||
:attributes=> { \
|
||||
'source-highlighter' => 'prettify', \
|
||||
'icons' => 'font', \
|
||||
'linkcss'=> 'true', \
|
||||
'copycss' => 'true', \
|
||||
'doctype' => 'book'})
|
||||
}
|
||||
end
|
||||
|
||||
guard 'livereload' do
|
||||
watch(%r{^.+\.(css|js|html)$})
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
[[building]]
|
||||
== Building
|
||||
|
||||
:jdkversion: 1.7
|
||||
|
||||
=== Basic Compile and Test
|
||||
|
||||
To build the source you will need to install JDK {jdkversion}.
|
||||
|
||||
The build uses the Maven wrapper so you don't have to install a specific version of Maven.
|
||||
To enable the tests, you should have https://github.com/mhart/kinesalite[Kinesalite] server running before building.
|
||||
|
||||
The main build command is
|
||||
|
||||
----
|
||||
$ ./mvnw clean install
|
||||
----
|
||||
|
||||
You can also add `-DskipTests` if you like, to avoid running the tests.
|
||||
|
||||
NOTE: You can also install Maven (>=3.3.3) yourself and run the `mvn` command in place of `./mvnw` in the examples below.
|
||||
If you do that you also might need to add `-P spring` if your local Maven settings do not contain repository declarations for spring pre-release artifacts.
|
||||
|
||||
NOTE: Be aware that you might need to increase the amount of memory available to Maven by setting a `MAVEN_OPTS` environment variable with a value like `-Xmx512m -XX:MaxPermSize=128m`.
|
||||
We try to cover this in the `.mvn` configuration, so if you find you have to do it to make a build succeed, please raise a ticket to get the settings added to source control.
|
||||
|
||||
|
||||
The projects that require middleware generally include a `docker-compose.yml`, so consider using https://docs.docker.com/compose[Docker Compose] to run the middleware servers in Docker containers.
|
||||
|
||||
=== Documentation
|
||||
|
||||
There is a "full" profile that will generate documentation.
|
||||
|
||||
=== Working with the code
|
||||
If you don't have an IDE preference we would recommend that you use https://www.springsource.com/developer/sts[Spring Tools Suite] or https://eclipse.org[Eclipse] when working with the code.
|
||||
We use the https://eclipse.org/m2e/[m2eclipe] eclipse plugin for maven support.
|
||||
Other IDEs and tools should also work without issue.
|
||||
|
||||
==== Importing into eclipse with m2eclipse
|
||||
We recommend the https://eclipse.org/m2e/[m2eclipe] eclipse plugin when working with eclipse.
|
||||
If you don't already have m2eclipse installed it is available from the "eclipse marketplace".
|
||||
|
||||
Unfortunately m2e does not yet support Maven 3.3, so once the projects are imported into Eclipse you will also need to tell m2eclipse to use the `.settings.xml` file for the projects.
|
||||
If you do not do this you may see many different errors related to the POMs in the projects.
|
||||
Open your Eclipse preferences, expand the Maven preferences, and select User Settings.
|
||||
In the User Settings field click Browse and navigate to the Spring Cloud project you imported selecting the `.settings.xml` file in that project.
|
||||
Click Apply and then OK to save the preference changes.
|
||||
|
||||
NOTE: Alternatively you can copy the repository settings from https://github.com/spring-cloud/spring-cloud-build/blob/main/.settings.xml[`.settings.xml`] into your own `~/.m2/settings.xml`.
|
||||
|
||||
==== Importing into eclipse without m2eclipse
|
||||
If you prefer not to use m2eclipse you can generate eclipse project metadata using the following command:
|
||||
|
||||
[indent=0]
|
||||
----
|
||||
$ ./mvnw eclipse:eclipse
|
||||
----
|
||||
|
||||
The generated eclipse projects can be imported by selecting `import existing projects` from the `file` menu.
|
||||
@@ -0,0 +1,26 @@
|
||||
[[contributing]]
|
||||
== Contributing
|
||||
|
||||
Spring Cloud is released under the non-restrictive Apache 2.0 license, and follows a very standard Github development process, using Github tracker for issues and merging pull requests into main.
|
||||
If you want to contribute even something trivial please do not hesitate, but follow the guidelines below.
|
||||
|
||||
=== Sign the Contributor License Agreement
|
||||
|
||||
Before we accept a non-trivial patch or pull request we will need you to sign the https://support.springsource.com/spring_committer_signup[contributor's agreement].
|
||||
Signing the contributor's agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do.
|
||||
Active contributors might be asked to join the core team, and given the ability to merge pull requests.
|
||||
|
||||
=== Code Conventions and Housekeeping
|
||||
|
||||
None of these is essential for a pull request, but they will all help.
|
||||
They can also be added after the original pull request but before a merge.
|
||||
|
||||
* Use the Spring Framework code format conventions. If you use Eclipse you can import formatter settings using the `eclipse-code-formatter.xml` file from the https://github.com/spring-cloud/build/tree/main/eclipse-coding-conventions.xml[Spring Cloud Build] project.
|
||||
If using IntelliJ, you can use the https://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter Plugin] to import the same file.
|
||||
* Make sure all new `.java` files to have a simple Javadoc class comment with at least an `@author` tag identifying you, and preferably at least a paragraph on what the class is for.
|
||||
* Add the ASF license header comment to all new `.java` files (copy from existing files in the project)
|
||||
* Add yourself as an `@author` to the .java files that you modify substantially (more than cosmetic changes).
|
||||
* Add some Javadocs and, if you change the namespace, some XSD doc elements.
|
||||
* A few unit tests would help a lot as well -- someone has to do it.
|
||||
* If no-one else is using your branch, please rebase it against the current main (or other target branch in the main project).
|
||||
* When writing a commit message please follow https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions], if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit message (where XXXX is the issue number).
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,14 @@
|
||||
<productname>Spring Cloud Stream Kinesis Binder</productname>
|
||||
<releaseinfo>{spring-cloud-stream-binder-kinesis-version}</releaseinfo>
|
||||
<copyright>
|
||||
<year>2017-2021</year>
|
||||
<holder>VMWare, Inc.</holder>
|
||||
</copyright>
|
||||
<legalnotice>
|
||||
<para>
|
||||
Copies of this document may be made for your own use and for distribution to
|
||||
others, provided that you do not charge any fee for such copies and further
|
||||
provided that each copy contains this Copyright Notice, whether distributed in
|
||||
print or electronically.
|
||||
</para>
|
||||
</legalnotice>
|
||||
@@ -0,0 +1,34 @@
|
||||
[[spring-cloud-stream-binder-kinesis-reference]]
|
||||
= Spring Cloud Stream AWS Kinesis Binder Reference Guide
|
||||
Artem Bilan
|
||||
:doctype: book
|
||||
:toc:
|
||||
:toclevels: 4
|
||||
:source-highlighter: prettify
|
||||
:numbered:
|
||||
:icons: font
|
||||
:hide-uri-scheme:
|
||||
:spring-cloud-stream-binder-kinesis-repo: snapshot
|
||||
:github-tag: main
|
||||
:spring-cloud-stream-binder-kinesis-docs-version: current
|
||||
:spring-cloud-stream-binder-kinesis-docs: https://docs.spring.io/spring-cloud-stream-binder-kinesis/docs/{spring-cloud-stream-binder-kinesis-docs-version}/reference
|
||||
:spring-cloud-stream-binder-kinesis-docs-current: https://docs.spring.io/spring-cloud-stream-binder-kinesis/docs/current-SNAPSHOT/reference/html/
|
||||
:github-repo: spring-cloud/spring-cloud-stream-binder-aws-kinesis
|
||||
:github-raw: https://raw.github.com/{github-repo}/{github-tag}
|
||||
:github-code: https://github.com/{github-repo}/tree/{github-tag}
|
||||
:github-wiki: https://github.com/{github-repo}/wiki
|
||||
:github-main-code: https://github.com/{github-repo}/tree/main
|
||||
:sc-ext: java
|
||||
// ======================================================================================
|
||||
|
||||
= Reference Guide
|
||||
include::overview.adoc[]
|
||||
|
||||
|
||||
= Appendices
|
||||
[appendix]
|
||||
include::building.adoc[]
|
||||
[appendix]
|
||||
include::contributing.adoc[]
|
||||
|
||||
// ======================================================================================
|
||||
@@ -0,0 +1,544 @@
|
||||
[partintro]
|
||||
--
|
||||
This guide describes the https://aws.amazon.com/kinesis/[AWS Kinesis] implementation of the Spring Cloud Stream Binder.
|
||||
It contains information about its design, usage and configuration options, as well as information on how the Stream Cloud Stream concepts map into AWS Kinesis specific constructs.
|
||||
--
|
||||
|
||||
== Usage
|
||||
|
||||
For using the AWS Kinesis Binder, you just need to add it to your Spring Cloud Stream application, using the following Maven coordinates:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-binder-kinesis</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
== Kinesis Binder Overview
|
||||
|
||||
The Spring Cloud Stream Binder for AWS Kinesis provides the binding implementation for the Spring Cloud Stream.
|
||||
This implementation uses Spring Integration AWS Kinesis Channel Adapters at its foundation.
|
||||
The following captures how the Kinesis Binder implementation maps each of the configured destination to a AWS Kinesis Streams:
|
||||
|
||||
.Kinesis Binder
|
||||
image::images/kinesis-binder.png[width=300,scaledwidth="50%"]
|
||||
|
||||
Unlike https://kafka.apache.org/[Apache Kafka] the AWS Kinesis doesn't provide out-of-the-box support for consumer groups.
|
||||
The support of this feature is implemented as a part of `MetadataStore` key for shard checkpoints in the `KinesisMessageDrivenChannelAdapter` - `[CONSUMER_GROUP]:[STREAM]:[SHARD_ID]`.
|
||||
In addition the `LockRegistry` is used to ensure exclusive access to each shard.
|
||||
This way only one channel adapter in the same consumer group will consumer messages from a single shard in the stream it is configured for.
|
||||
|
||||
The partitioning logic in AWS Kinesis is similar to the Apache Kafka support, but with slightly different logic.
|
||||
The `partitionKey` on the producer side determines which shard in the stream the data record is assigned to.
|
||||
Partition keys are Unicode strings with a maximum length limit of 256 characters for each key.
|
||||
AWS Kinesis uses the partition key as input to a hash function that maps the partition key and associated data to a specific shard.
|
||||
Specifically, an MD5 hash function is used to map partition keys to 128-bit integer values and to map associated data records to shards.
|
||||
As a result of this hashing mechanism, all data records with the same partition key map to the same shard within the stream.
|
||||
But at the same time we can't select target shard to send explicitly.
|
||||
Although calculating the hash manually (and use `explicitHashKeyExpression` for producer, respectively), we may track the target shard by inclusion into its `HashKeyRange`.
|
||||
|
||||
By default partition key is a result of the `Object.hash()` from the message `payload`.
|
||||
|
||||
The Spring Cloud Stream partition handling logic is excluded in case of AWS Kinesis Binder since it is out of use and the provided `producer.partitionKeyExpression` is propagated to the `KinesisMessageHandler` directly.
|
||||
|
||||
On the consumer side the `instanceCount` and `instanceIndex` are used to distribute shards between consumers in group evenly.
|
||||
This has an effect only for regular `KinesisMessageDrivenChannelAdapter` which can assign specific shards for the target Kinesis consumer.
|
||||
With Kinesis Client Library we can only subscriber to the provided stream and shards distribution is done by that client.
|
||||
See more information in the Kinesis Client Library https://docs.aws.amazon.com/streams/latest/dev/developing-consumers-with-kcl.html[documentation].
|
||||
|
||||
== Consumer Groups
|
||||
Consumer groups are implemented with focus on High availability, Message ordering and guaranteed Message delivery in Spring cloud stream.
|
||||
A `single consumer` for the message is ensured by https://docs.spring.io/spring-cloud-stream/docs/Elmhurst.RELEASE/reference/htmlsingle/#consumer-groups[consumer group abstraction].
|
||||
|
||||
To have a highly available consumer group for your kinesis stream:
|
||||
|
||||
- Ensure all instances of your consumer applications use a shared `DynamoDbMetadataStore` and `DynamoDbLockRegistry` (See below for configuration options).
|
||||
- Use same group name for the channel in all application instances by using property `spring.cloud.stream.bindings.<bindingTarget>.group`.
|
||||
|
||||
These configurations alone guarantee HA, message ordering and guaranteed message delivery.
|
||||
However, even distribution across instances is not guaranteed as of now.
|
||||
There is a very high chance that a single instance in a consumer group will pick up all the shards for consuming.
|
||||
But, when that instance goes down (couldn't send heartbeat for any reason), other instance in the consumer group will start processing from the last checkpoint of the previous consumer (for shardIterator type TRIM_HORIZON).
|
||||
|
||||
So, configuring consumer concurrency is important to achieve throughput.
|
||||
It can be configured using `spring.cloud.stream.bindings.<bindingTarget>.consumer.concurrency`.
|
||||
|
||||
=== Static shard distribution within a single consumer group
|
||||
It is possible to evenly distribute shard across all instances within a single consumer group.
|
||||
This done by configuring:
|
||||
|
||||
- `spring.cloud.stream.instanceCount=` to number of instances
|
||||
- `spring.cloud.stream.instanceIndex=` current instance's index
|
||||
|
||||
The only way to achieve HA in this case is that, when an instance processing a particular shard goes down, another instance must have `spring.cloud.stream.instanceIndex=` to be the same as the failed instance's index to start processing from those shards.
|
||||
|
||||
== Configuration Options
|
||||
|
||||
This section contains settings specific to the Kinesis Binder and bound channels.
|
||||
|
||||
For general binding configuration options and properties, please refer to the https://docs.spring.io/spring-cloud-stream/docs/current/reference/htmlsingle/#_configuration_options[Spring Cloud Stream core documentation].
|
||||
|
||||
[[kinesis-binder-properties]]
|
||||
=== Kinesis Binder Properties
|
||||
|
||||
The following properties are available for Kinesis Binder configuration, which start with the `spring.cloud.stream.kinesis.binder.` prefix
|
||||
|
||||
headers::
|
||||
The set of custom headers to transfer over AWS Kinesis
|
||||
+
|
||||
Default: "correlationId", "sequenceSize", "sequenceNumber", "contentType", "originalContentType".
|
||||
describeStreamBackoff::
|
||||
The amount of time in milliseconds in between retries for the `DescribeStream` operation
|
||||
+
|
||||
Default: `1000`.
|
||||
describeStreamRetries::
|
||||
The amount of times the consumer will retry a `DescribeStream` operation waiting for the stream to be in `ACTIVE` state
|
||||
+
|
||||
Default: `50`.
|
||||
autoCreateStream::
|
||||
If set to `true`, the binder will create the stream automatically.
|
||||
If set to `false`, the binder will rely on the stream being already created.
|
||||
+
|
||||
Default: `true`
|
||||
autoAddShards::
|
||||
If set to `true`, the binder will create new shards automatically.
|
||||
If set to `false`, the binder will rely on the shard size of the stream being already configured.
|
||||
If the shard count of the target stream is smaller than the expected value, the binder will ignore that value
|
||||
+
|
||||
Default: `false`
|
||||
minShardCount::
|
||||
Effective only if `autoAddShards` is set to `true`.
|
||||
The minimum number of shards that the binder will configure on the stream from which it produces/consumes data.
|
||||
It can be superseded by the `partitionCount` setting of the producer or by the value of `instanceCount * concurrency` settings of the producer (if either is larger)
|
||||
+
|
||||
Default: `1`
|
||||
kplKclEnabled::
|
||||
Enable the usage of https://docs.aws.amazon.com/streams/latest/dev/developing-consumers-with-kcl.html[Kinesis Client Library] / https://docs.aws.amazon.com/streams/latest/dev/developing-producers-with-kpl.html[Kinesis Producer Library] for all message consumption and production
|
||||
+
|
||||
Default: `false`
|
||||
|
||||
=== MetadataStore
|
||||
Support for consumer groups is implemented using https://github.com/spring-projects/spring-integration-aws#metadata-store-for-amazon-dynamodb[DynamoDbMetadataStore].
|
||||
The `partitionKey` name used in the table is `KEY`.
|
||||
This is not configurable.
|
||||
|
||||
DynamoDB Checkpoint properties are prefixed with `spring.cloud.stream.kinesis.binder.checkpoint.`
|
||||
|
||||
table::
|
||||
The name to give the DynamoDb table
|
||||
+
|
||||
Default: `SpringIntegrationMetadataStore`
|
||||
createDelay::
|
||||
The amount of time in seconds between each polling attempt while waiting for the checkpoint DynamoDB table to be created
|
||||
+
|
||||
Default: `1`
|
||||
createRetries::
|
||||
The amount of times the consumer will poll DynamoDB while waiting for the checkpoint table to be created
|
||||
+
|
||||
Default: `25`
|
||||
billingMode::
|
||||
The Billing Mode of the DynamoDB table. See https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html#HowItWorks.OnDemand[DynamoDB On-Demand Mode]. Possible values are `provisioned` and `payPerRequest`. If left empty or set to `payPerRequest` both `readCapacity` and `writeCapacity` are ignored
|
||||
+
|
||||
Default: `payPerRequest`
|
||||
readCapacity::
|
||||
The Read capacity of the DynamoDb table.
|
||||
See https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html#HowItWorks.ProvisionedThroughput.Manual[DynamoDB Provisioned Throughput]. This property is used only when `billingMode` is set to `provisioned`
|
||||
+
|
||||
Default: `1`
|
||||
writeCapacity::
|
||||
The write capacity of the DynamoDb table.
|
||||
See https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html#HowItWorks.ProvisionedThroughput.Manual[DynamoDB Provisioned Throughput]. This property is used only when `billingMode` is set to `provisioned`
|
||||
+
|
||||
Default: `1`
|
||||
timeToLive::
|
||||
A period in seconds for items expiration.
|
||||
See https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html[DynamoDB TTL]
|
||||
+
|
||||
No default - means no records expiration.
|
||||
|
||||
=== LockRegistry
|
||||
LockRegistry is used to ensure exclusive access to each shard so that, only one channel adapter in the same consumer group will consumer messages from a single shard in the stream.
|
||||
This is implemented using https://github.com/spring-projects/spring-integration-aws#lock-registry-for-amazon-dynamodb[DynamoDbLockRegistry]
|
||||
|
||||
DynamoDB `LockRegistry` properties are prefixed with `spring.cloud.stream.kinesis.binder.locks.`
|
||||
|
||||
table::
|
||||
The name to give the DynamoDB table
|
||||
+
|
||||
Default: `SpringIntegrationLockRegistry`
|
||||
billingMode::
|
||||
The Billing Mode of the DynamoDB table. See https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html#HowItWorks.OnDemand[DynamoDB On-Demand Mode]. Possible values are `provisioned` and `payPerRequest`. If left empty or set to `payPerRequest` both `readCapacity` and `writeCapacity` are ignored
|
||||
+
|
||||
Default: `payPerRequest`
|
||||
readCapacity::
|
||||
The Read capacity of the DynamoDB table.
|
||||
See https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html#HowItWorks.ProvisionedThroughput.Manual[DynamoDB Provisioned Throughput]. This property is used only when `billingMode` is set to `provisioned`
|
||||
+
|
||||
Default: `1`
|
||||
writeCapacity::
|
||||
The write capacity of the DynamoDb table.
|
||||
See https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html#HowItWorks.ProvisionedThroughput.Manual[DynamoDB Provisioned Throughput]. This property is used only when `billingMode` is set to `provisioned`
|
||||
+
|
||||
Default: `1`
|
||||
leaseDuration::
|
||||
The length of time that the lease for the lock will be granted for.
|
||||
If this is set to, for example, 30 seconds, then the lock will expire if the heartbeat is not sent for at least 30 seconds (which would happen if the box or the heartbeat thread dies, for example.)
|
||||
+
|
||||
Default: `20`
|
||||
heartbeatPeriod::
|
||||
How often to update DynamoDB to note that the instance is still running (recommendation is to make this at least 3 times smaller than the `leaseDuration` - for example `heartBeatPeriod=1` second, `leaseDuration=10` seconds could be a reasonable configuration, make sure to include a buffer for network latency.)
|
||||
+
|
||||
Default: `5`
|
||||
refreshPeriod::
|
||||
How long to wait before trying to get the lock again (if set to 10 seconds, for example, it would attempt to do so every 10 seconds)
|
||||
+
|
||||
Default: `1000`
|
||||
partitionKey::
|
||||
The partition key name of the table.
|
||||
+
|
||||
Default: `lockKey`
|
||||
sortKeyName::
|
||||
The sort key name for DynamoDB table partitioning.
|
||||
+
|
||||
Default: `sortKey`
|
||||
sortKey::
|
||||
The sort key to try and acquire the lock on (specify if and only if the table has sort keys)
|
||||
+
|
||||
Default: `SpringIntegrationLocks`
|
||||
|
||||
=== Kinesis Consumer Properties
|
||||
|
||||
The following properties are available for Kinesis consumers only and must be prefixed with `spring.cloud.stream.kinesis.bindings.<channel-name>.consumer`
|
||||
|
||||
startTimeout::
|
||||
The amount of time to wait for the consumer to start, in milliseconds.
|
||||
+
|
||||
Default: `60000`.
|
||||
listenerMode::
|
||||
The mode in which records are processed.
|
||||
If `record`, each `Message` will contain `byte[]` from a single `Record.data`.
|
||||
If `batch`, each `Message` will contain a `List<byte[]>` extracted from the consumed records.
|
||||
When `useNativeDecoding = true` is used on the consumer together with the `listenerMode = batch`, there is no any out-of-the-box conversion happened and a result message contains a payload like `List<com.amazonaws.services.kinesis.model.Record>`.
|
||||
It's up to target application to convert those records manually.
|
||||
+
|
||||
Default: `record`
|
||||
checkpointMode::
|
||||
The mode in which checkpoints are updated.
|
||||
If `record`, checkpoints occur after each record is processed (but this option is only effective if `listenerMode` is set to `record`). If `batch`, checkpoints occur after each batch of records is processed.
|
||||
If `manual`, checkpoints occur on demand via the `Checkpointer` callback.
|
||||
If `periodic`, checkpoints occurs at specified time interval (from `interval` property in checkpoint configuration)
|
||||
+
|
||||
Default: `batch`
|
||||
checkpointInterval::
|
||||
The interval, in milliseconds, between two checkpoints when checkpoint mode is `periodic`.
|
||||
+
|
||||
Default - `5000`
|
||||
workerId::
|
||||
The worker identifier used to distinguish different workers/processes (only used when Kinesis Client Library is enabled).
|
||||
+
|
||||
No default - if not set, default value inside spring-integration-aws will be used (random UUID).
|
||||
recordsLimit::
|
||||
The maximum number of records to poll per `GetRecords` request.
|
||||
Must not be greater than `10000`.
|
||||
+
|
||||
Default: `10000`
|
||||
idleBetweenPolls::
|
||||
The sleep interval used in the main loop between shards polling cycles, in milliseconds. Must not be less than `250`.
|
||||
+
|
||||
Default: `1000`
|
||||
consumerBackoff::
|
||||
The amount of time the consumer will wait to attempt another `GetRecords` operation after a read with no results, in milliseconds.
|
||||
+
|
||||
Default: `1000`
|
||||
shardIteratorType::
|
||||
The `com.amazonaws.services.kinesis.model.ShardIteratorType` name with an optional `sequenceNumber` for the `AT_SEQUENCE_NUMBER/AFTER_SEQUENCE_NUMBER` or milliseconds for the `AT_TIMESTAMP` after `:`.
|
||||
For example: `AT_TIMESTAMP:1515090166767`.
|
||||
+
|
||||
Default: `LATEST` for anonymous groups and `TRIM_HORIZON` otherwise.
|
||||
|
||||
NOTE: When `TRIM_HORIZON` shard iterator type is used, we need to take into account the time lag which happens during pointing the `ShardIterator` to the last untrimmed record in the shard in the system (the oldest data record in the shard).
|
||||
So the `getRecords()` will move from that point to the last point, which takes time.
|
||||
It is by default 1 day and it can be extended to 7 days.
|
||||
This happens only for new consumer groups.
|
||||
Any subsequent starts of the consumer in the same group are adjusted according the stored checkpoint via `AFTER_SEQUENCE_NUMBER` iterator type.
|
||||
|
||||
dynamoDbStreams::
|
||||
The `boolean` flag indicating that Kinesis consumer channel adapter should adapt DynamoDB Streams functionality instead of regular Kinesis streams.
|
||||
The `spring.cloud.stream.bindings.<bindingTarget>.destination` value must be a DynamoDB table name.
|
||||
Default: `false`.
|
||||
|
||||
Starting with version 2.0.1, beans of `KinesisClientLibConfiguration` type can be provided in the application context to have a full control over Kinesis Client Library configuration options.
|
||||
The stream and consumer group (plus workerId) must be provided in the respective `KinesisClientLibConfiguration` bean.
|
||||
When `KclMessageDrivenChannelAdapter` endpoint is configured in the binder, it selects an appropriate `KinesisClientLibConfiguration` from the application context according a destination (stream) for binding.
|
||||
If there is no `KinesisClientLibConfiguration` bean for its stream, the `KclMessageDrivenChannelAdapter` falls back to original configuration with defaults options for its internal `KinesisClientLibConfiguration`.
|
||||
|
||||
shardId::
|
||||
An explicit shard id to consume from.
|
||||
|
||||
NOTE: Kinesis Client Library does not support a configuration for a specific shard.
|
||||
When `shardId` property is used, it is ignored for Kinesis Client Library and standard stream consumer distribution is applied.
|
||||
Also, in case of an `instanceCount > 1`, application will throw validation exception.
|
||||
The `instanceCount` and `shardId` are considered as mutually exclusive.
|
||||
|
||||
=== Kinesis Producer Properties
|
||||
|
||||
The following properties are available for Kinesis producers only and must be prefixed with `spring.cloud.stream.kinesis.bindings.<bindingTarget>.producer.`.
|
||||
|
||||
sync::
|
||||
Whether the producer should act in a synchronous manner with respect to writing records into a stream.
|
||||
If true, the producer will wait for a response from Kinesis after a `PutRecord` operation.
|
||||
+
|
||||
Default: `false`
|
||||
sendTimeout::
|
||||
Effective only if `sync` is set to `true`. The amount of time to wait for a response from Kinesis after a `PutRecord` operation, in milliseconds.
|
||||
+
|
||||
Default: `10000`
|
||||
|
||||
Also, if you'd like to produce a batch of records into Kinesis stream, the message payload must be as a `PutRecordsRequest` instance and general Spring Cloud Stream producer property `useNativeEncoding` must be set to `true`, so Spring Cloud Stream won't try to convert a `PutRecordsRequest` into a `byte[]`. The content of the `PutRecordsRequest` is now end-user responsibility.
|
||||
[[kinesis-error-channels]]
|
||||
== Error Channels
|
||||
|
||||
The binder can be configured to send producer exceptions to an error channel.
|
||||
See https://docs.spring.io/spring-cloud-stream/docs/current/reference/htmlsingle/#_spring_integration_error_channel_support[the section on Spring Cloud error channel support] for more information.
|
||||
|
||||
The payload of the `ErrorMessage` for a send failure is an `AwsRequestFailureException` with properties:
|
||||
|
||||
* `failedMessage` - the spring-messaging `Message<?>` that failed to be sent.
|
||||
* `request` - the raw `AmazonWebServiceRequest` (either `PutRecordRequest` or `PutRecordsRequest`) that was created from the `failedMessage`.
|
||||
|
||||
There is no automatic handling of these exceptions (such as sending to a dead letter queue), but you can consume these exceptions with your own Spring Integration flow.
|
||||
|
||||
[[dynamodb-streams]]
|
||||
== DynamoDB Streams
|
||||
|
||||
Starting with version 1.2, the `KinesisMessageChannelBinder` supports DynamoDB Streams for Kinesis consumer channel adapter.
|
||||
The functionality is based on the https://github.com/awslabs/dynamodb-streams-kinesis-adapter[DynamoDB Streams Kinesis Adapter] and every Kinesis consumer endpoint selects an appropriate Kinesis Client according the `spring.cloud.stream.kinesis.bindings.<bindingTarget>.consumer.dynamoDbStreams` boolean flag.
|
||||
In this case a value for `spring.cloud.stream.bindings.<bindingTarget>.destination` must be a DynamoDB table name.
|
||||
|
||||
[[optional-resources]]
|
||||
== Optional Resources
|
||||
|
||||
Starting with version 1.2, if your Spring Cloud Stream application delivered only in the `source` role, the extra beans, required for `sink` (or Kinesis consumers), are not going to be registered in the application context and, therefore, no need to worry about their resources on AWS.
|
||||
The story is about DynamoDB and Cloud Watch.
|
||||
|
||||
[[aws-roles-and-policies]]
|
||||
== AWS Roles and Policies
|
||||
|
||||
In order to be able to run properly on AWS, the role that will be used by the application needs to have a set of policies configured.
|
||||
Here are the policies statements that your application role need:
|
||||
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"kinesis:SubscribeToShard",
|
||||
"kinesis:DescribeStreamSummary",
|
||||
"kinesis:DescribeStreamConsumer",
|
||||
"kinesis:GetShardIterator",
|
||||
"kinesis:GetRecords",
|
||||
"kinesis:PutRecords",
|
||||
"kinesis:DescribeStream"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:kinesis:<region>:<account_number>:*/*/consumer/*:*",
|
||||
"arn:aws:kinesis:<region>:<account_number>:stream/<stream_name>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "kinesis:DescribeLimits",
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Sid": "DynamoDB",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"dynamodb:BatchGetItem",
|
||||
"dynamodb:BatchWriteItem",
|
||||
"dynamodb:PutItem",
|
||||
"dynamodb:GetItem",
|
||||
"dynamodb:Scan",
|
||||
"dynamodb:Query",
|
||||
"dynamodb:UpdateItem"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:dynamodb:<region>:<account>:table/<name-of-metadata-table>",
|
||||
"arn:aws:dynamodb:<region>:<account>:table/<name-of-lock-table>"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
----
|
||||
|
||||
Keep in mind that these are only the policies to allow the application to consume/produce records from/to Kinesis.
|
||||
If you're going to allow spring-cloud-stream-binder-kinesis to create the resources for you, you'll need an extra set of policies.
|
||||
[source,json]
|
||||
----
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"dynamodb:CreateTable",
|
||||
"kinesis:CreateStream",
|
||||
"kinesis:UpdateShardCount",
|
||||
"kinesis:EnableEnhancedMonitoring",
|
||||
"kinesis:DisableEnhancedMonitoring",
|
||||
"dynamodb:DeleteTable",
|
||||
"dynamodb:UpdateTable"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:dynamodb:<region>:<account>:table/<table_name>",
|
||||
"arn:aws:kinesis:<region>:<account>:stream/<stream_name>"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
----
|
||||
|
||||
[[running-locally-with-localstack]]
|
||||
== Running locally with localstack
|
||||
|
||||
Sometimes we don't have the necessary permissions to connect to the real Kinesis and DynamoDB from our developer's machine.
|
||||
In moments like this, it's pretty useful to setup Localstack in your project, so you can run everything locally, without
|
||||
having to worry about permissions and enterprise restrictions.
|
||||
|
||||
Create a *docker-compose.yaml* file, in the root of your project, to quickly start localstack
|
||||
|
||||
[source, yaml]
|
||||
----
|
||||
version: '3.5'
|
||||
|
||||
services:
|
||||
localstack:
|
||||
image: localstack/localstack:0.12.10
|
||||
environment:
|
||||
- AWS_DEFAULT_REGION=sa-east-1
|
||||
- EDGE_PORT=4566
|
||||
- SERVICES=kinesis, dynamodb
|
||||
ports:
|
||||
- '4566:4566'
|
||||
volumes:
|
||||
- localstack:/tmp/localstack
|
||||
- './setup-localstack.sh:/docker-entrypoint-initaws.d/setup-localstack.sh'
|
||||
|
||||
volumes:
|
||||
localstack:
|
||||
----
|
||||
|
||||
After that, create a script called *setup-localstack.sh*, in the root directory, that will contain the script to create the
|
||||
Kinesis Stream, and the 2 DynamoDB Tables
|
||||
|
||||
[source, shell script]
|
||||
----
|
||||
awslocal kinesis create-stream --stream-name my-test-stream --shard-count 1
|
||||
|
||||
awslocal dynamodb create-table \
|
||||
--table-name spring-stream-lock-registry \
|
||||
--attribute-definitions AttributeName=lockKey,AttributeType=S AttributeName=sortKey,AttributeType=S \
|
||||
--key-schema AttributeName=lockKey,KeyType=HASH AttributeName=sortKey,KeyType=RANGE \
|
||||
--provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \
|
||||
--tags Key=Owner,Value=localstack
|
||||
|
||||
awslocal dynamodb create-table \
|
||||
--table-name spring-stream-metadata \
|
||||
--attribute-definitions AttributeName=KEY,AttributeType=S \
|
||||
--key-schema AttributeName=KEY,KeyType=HASH \
|
||||
--provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \
|
||||
--tags Key=Owner,Value=localstack
|
||||
|
||||
awslocal dynamodb list-tables
|
||||
awslocal kinesis list-streams
|
||||
----
|
||||
|
||||
Since this file is being mapped to the localstack image, the container will automatically run this script the first
|
||||
time you run the container.
|
||||
|
||||
To run localstack, just execute
|
||||
[source, shell script]
|
||||
----
|
||||
docker-compose up -d
|
||||
----
|
||||
Your local AWS Endpoint is now available at http://localhost:4566
|
||||
|
||||
To put records into your test stream, just run
|
||||
[source, shell script]
|
||||
----
|
||||
aws --endpoint-url=http://localhost:4566 kinesis put-record --stream-name my-test-stream --partition-key 1 --data <base64-encoded-data>
|
||||
----
|
||||
|
||||
[[telling-the-binder-to-use-your-local-endpoint]]
|
||||
=== Telling the binder to use your local endpoint
|
||||
|
||||
By default, the Kinesis and DynamoDB Client will try to hit the real AWS Endpoint. To change this behavior,
|
||||
you have to declare a new @Bean, and override the endpoint.
|
||||
|
||||
For example:
|
||||
|
||||
[source, kotlin]
|
||||
----
|
||||
@Configuration
|
||||
@Profile("local")
|
||||
class DynamoDBConfigLocal {
|
||||
@Value("\${cloud.aws.region.static}")
|
||||
val region: String = ""
|
||||
|
||||
private val endpointUrl: String = "http://localhost:4566"
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
fun amazonDynamoDBAsync(): AmazonDynamoDBAsync {
|
||||
return AmazonDynamoDBAsyncClientBuilder.standard()
|
||||
.withEndpointConfiguration(AwsClientBuilder.EndpointConfiguration(this.endpointUrl, region))
|
||||
.build()
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
[source, kotlin]
|
||||
----
|
||||
@Configuration
|
||||
@Profile("local")
|
||||
class KinesisConfigLocal {
|
||||
@Value("\${cloud.aws.region.static}")
|
||||
val region: String = ""
|
||||
|
||||
private val endpointUrl: String = "http://localhost:4566"
|
||||
|
||||
@Bean
|
||||
fun amazonKinesis(awsCredentialsProvider: AWSCredentialsProvider): AmazonKinesisAsync {
|
||||
return AmazonKinesisAsyncClientBuilder
|
||||
.standard()
|
||||
.withCredentials(awsCredentialsProvider)
|
||||
.withEndpointConfiguration(AwsClientBuilder.EndpointConfiguration(endpointUrl, region))
|
||||
.build()
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Now, remember to pass the following environment variables when running locally:
|
||||
[source, shell script]
|
||||
----
|
||||
SPRING_PROFILES_ACTIVE=local AWS_CBOR_DISABLE=true gradle bootRun
|
||||
----
|
||||
This will make sure that these beans are only instantiated when running locally, and will also disable CBOR, which is not
|
||||
supported for the localstack's kinesis stream.
|
||||
|
||||
[[health-indicator]]
|
||||
== Kinesis Binder Health Indicator
|
||||
|
||||
Version 2.0 has introduced a `KinesisBinderHealthIndicator` implementation which is a part of `BindersHealthContributor` composition under the `binders` path.
|
||||
An out-of-the-box implementation iterates over Kinesis streams involved in the binder configuration calling a `describeStream` command against them.
|
||||
If any of streams doesn't exist the health is treated as `DOWN`.
|
||||
If `LimitExceededException` is thrown according `describeStream` limitations, the `KinesisBinderHealthIndicator` tries over again after one second interval.
|
||||
Ony when all the configured stream are described properly the `UP` health is returned.
|
||||
You can override out-of-the-box implementation provided your own bean with the `kinesisBinderHealthIndicator` name.
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
base_dir = File.join(File.dirname(__FILE__),'../../..')
|
||||
src_dir = File.join(base_dir, "/src/main/asciidoc")
|
||||
require 'asciidoctor'
|
||||
require 'optparse'
|
||||
|
||||
options = {}
|
||||
file = "#{src_dir}/README.adoc"
|
||||
|
||||
OptionParser.new do |o|
|
||||
o.on('-o OUTPUT_FILE', 'Output file (default is stdout)') { |file| options[:to_file] = file unless file=='-' }
|
||||
o.on('-h', '--help') { puts o; exit }
|
||||
o.parse!
|
||||
end
|
||||
|
||||
file = ARGV[0] if ARGV.length>0
|
||||
|
||||
# Copied from https://github.com/asciidoctor/asciidoctor-extensions-lab/blob/master/scripts/asciidoc-coalescer.rb
|
||||
doc = Asciidoctor.load_file file, safe: :unsafe, header_only: true, attributes: options[:attributes]
|
||||
header_attr_names = (doc.instance_variable_get :@attributes_modified).to_a
|
||||
header_attr_names.each {|k| doc.attributes[%(#{k}!)] = '' unless doc.attr? k }
|
||||
attrs = doc.attributes
|
||||
attrs['allow-uri-read'] = true
|
||||
puts attrs
|
||||
|
||||
out = "// Do not edit this file (e.g. go instead to src/main/asciidoc)\n\n"
|
||||
doc = Asciidoctor.load_file file, safe: :unsafe, parse: false, attributes: attrs
|
||||
out << doc.reader.read
|
||||
|
||||
unless options[:to_file]
|
||||
puts out
|
||||
else
|
||||
File.open(options[:to_file],'w+') do |file|
|
||||
file.write(out)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-binder-kinesis-parent</artifactId>
|
||||
<version>4.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>spring-cloud-stream-binder-kinesis</artifactId>
|
||||
|
||||
<packaging>jar</packaging>
|
||||
<name>spring-cloud-stream-binder-kinesis</name>
|
||||
<description>AWS Kinesis Binder Implementation</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>amazon-kinesis-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>amazon-kinesis-producer</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.integration</groupId>
|
||||
<artifactId>spring-integration-aws</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.awspring.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-aws</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-dynamodb</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>dynamodb-lock-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-kinesis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>dynamodb-streams-kinesis-adapter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-binder-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-test-support-internal</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>localstack</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2017-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.amazonaws.services.kinesis.AmazonKinesisAsync;
|
||||
import com.amazonaws.services.kinesis.model.LimitExceededException;
|
||||
import com.amazonaws.services.kinesis.model.ListShardsRequest;
|
||||
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
|
||||
/**
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
public class KinesisBinderHealthIndicator implements HealthIndicator {
|
||||
|
||||
private final KinesisMessageChannelBinder kinesisMessageChannelBinder;
|
||||
|
||||
public KinesisBinderHealthIndicator(KinesisMessageChannelBinder kinesisMessageChannelBinder) {
|
||||
this.kinesisMessageChannelBinder = kinesisMessageChannelBinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Health health() {
|
||||
AmazonKinesisAsync amazonKinesis = this.kinesisMessageChannelBinder.getAmazonKinesis();
|
||||
List<String> streamsInUse = new ArrayList<>(this.kinesisMessageChannelBinder.getStreamsInUse());
|
||||
for (String stream : streamsInUse) {
|
||||
while (true) {
|
||||
try {
|
||||
amazonKinesis.listShards(new ListShardsRequest().withStreamName(stream).withMaxResults(1));
|
||||
break;
|
||||
}
|
||||
catch (LimitExceededException ex) {
|
||||
try {
|
||||
TimeUnit.SECONDS.sleep(1);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return Health.down(ex).build();
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return Health.down(ex).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return Health.up().build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
/*
|
||||
* Copyright 2017-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.amazonaws.auth.AWSCredentialsProvider;
|
||||
import com.amazonaws.services.cloudwatch.AmazonCloudWatch;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBStreams;
|
||||
import com.amazonaws.services.dynamodbv2.model.DescribeTableResult;
|
||||
import com.amazonaws.services.dynamodbv2.streamsadapter.AmazonDynamoDBStreamsAdapterClient;
|
||||
import com.amazonaws.services.kinesis.AmazonKinesis;
|
||||
import com.amazonaws.services.kinesis.AmazonKinesisAsync;
|
||||
import com.amazonaws.services.kinesis.clientlibrary.lib.worker.InitialPositionInStream;
|
||||
import com.amazonaws.services.kinesis.clientlibrary.lib.worker.KinesisClientLibConfiguration;
|
||||
import com.amazonaws.services.kinesis.model.InvalidArgumentException;
|
||||
import com.amazonaws.services.kinesis.model.Shard;
|
||||
import com.amazonaws.services.kinesis.model.ShardIteratorType;
|
||||
import com.amazonaws.services.kinesis.producer.KinesisProducer;
|
||||
import com.amazonaws.services.kinesis.producer.KinesisProducerConfiguration;
|
||||
|
||||
import org.springframework.cloud.stream.binder.AbstractMessageChannelBinder;
|
||||
import org.springframework.cloud.stream.binder.BinderHeaders;
|
||||
import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider;
|
||||
import org.springframework.cloud.stream.binder.ExtendedConsumerProperties;
|
||||
import org.springframework.cloud.stream.binder.ExtendedProducerProperties;
|
||||
import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder;
|
||||
import org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy;
|
||||
import org.springframework.cloud.stream.binder.kinesis.adapter.SpringDynamoDBAdapterClient;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisBinderConfigurationProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisConsumerProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisExtendedBindingProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisProducerProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.provisioning.KinesisConsumerDestination;
|
||||
import org.springframework.cloud.stream.binder.kinesis.provisioning.KinesisStreamProvisioner;
|
||||
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
|
||||
import org.springframework.cloud.stream.provisioning.ProducerDestination;
|
||||
import org.springframework.cloud.stream.provisioning.ProvisioningException;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.integration.aws.inbound.kinesis.KclMessageDrivenChannelAdapter;
|
||||
import org.springframework.integration.aws.inbound.kinesis.KinesisMessageDrivenChannelAdapter;
|
||||
import org.springframework.integration.aws.inbound.kinesis.KinesisMessageHeaderErrorMessageStrategy;
|
||||
import org.springframework.integration.aws.inbound.kinesis.KinesisShardOffset;
|
||||
import org.springframework.integration.aws.outbound.AbstractAwsMessageHandler;
|
||||
import org.springframework.integration.aws.outbound.KinesisMessageHandler;
|
||||
import org.springframework.integration.aws.outbound.KplMessageHandler;
|
||||
import org.springframework.integration.core.MessageProducer;
|
||||
import org.springframework.integration.expression.ExpressionUtils;
|
||||
import org.springframework.integration.expression.FunctionExpression;
|
||||
import org.springframework.integration.metadata.ConcurrentMetadataStore;
|
||||
import org.springframework.integration.support.ErrorMessageStrategy;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.integration.support.locks.LockRegistry;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.messaging.support.InterceptableChannel;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
*
|
||||
* The Spring Cloud Stream Binder implementation for AWS Kinesis.
|
||||
*
|
||||
* @author Peter Oates
|
||||
* @author Artem Bilan
|
||||
* @author Arnaud Lecollaire
|
||||
* @author Dirk Bonhomme
|
||||
* @author Asiel Caballero
|
||||
* @author Dmytro Danilenkov
|
||||
*
|
||||
*/
|
||||
public class KinesisMessageChannelBinder extends
|
||||
AbstractMessageChannelBinder<ExtendedConsumerProperties<KinesisConsumerProperties>,
|
||||
ExtendedProducerProperties<KinesisProducerProperties>, KinesisStreamProvisioner>
|
||||
implements
|
||||
ExtendedPropertiesBinder<MessageChannel, KinesisConsumerProperties, KinesisProducerProperties> {
|
||||
|
||||
private static final ErrorMessageStrategy ERROR_MESSAGE_STRATEGY = new KinesisMessageHeaderErrorMessageStrategy();
|
||||
|
||||
private final List<String> streamsInUse = new ArrayList<>();
|
||||
|
||||
private final KinesisBinderConfigurationProperties configurationProperties;
|
||||
|
||||
private final AmazonDynamoDBStreamsAdapterClient dynamoDBStreamsAdapter;
|
||||
|
||||
private final AmazonKinesisAsync amazonKinesis;
|
||||
|
||||
private final AWSCredentialsProvider awsCredentialsProvider;
|
||||
|
||||
private final AmazonCloudWatch cloudWatchClient;
|
||||
|
||||
private final AmazonDynamoDB dynamoDBClient;
|
||||
|
||||
private KinesisExtendedBindingProperties extendedBindingProperties = new KinesisExtendedBindingProperties();
|
||||
|
||||
@Nullable
|
||||
private ConcurrentMetadataStore checkpointStore;
|
||||
|
||||
@Nullable
|
||||
private LockRegistry lockRegistry;
|
||||
|
||||
@Nullable
|
||||
private KinesisProducerConfiguration kinesisProducerConfiguration;
|
||||
|
||||
private EvaluationContext evaluationContext;
|
||||
|
||||
private List<KinesisClientLibConfiguration> kinesisClientLibConfigurations;
|
||||
|
||||
public KinesisMessageChannelBinder(KinesisBinderConfigurationProperties configurationProperties,
|
||||
KinesisStreamProvisioner provisioningProvider, AmazonKinesisAsync amazonKinesis,
|
||||
AWSCredentialsProvider awsCredentialsProvider,
|
||||
@Nullable AmazonDynamoDB dynamoDBClient,
|
||||
@Nullable AmazonDynamoDBStreams dynamoDBStreams,
|
||||
@Nullable AmazonCloudWatch cloudWatchClient) {
|
||||
|
||||
super(headersToMap(configurationProperties), provisioningProvider);
|
||||
Assert.notNull(amazonKinesis, "'amazonKinesis' must not be null");
|
||||
Assert.notNull(awsCredentialsProvider, "'awsCredentialsProvider' must not be null");
|
||||
this.configurationProperties = configurationProperties;
|
||||
this.amazonKinesis = amazonKinesis;
|
||||
this.cloudWatchClient = cloudWatchClient;
|
||||
this.dynamoDBClient = dynamoDBClient;
|
||||
this.awsCredentialsProvider = awsCredentialsProvider;
|
||||
|
||||
if (dynamoDBStreams != null) {
|
||||
this.dynamoDBStreamsAdapter = new SpringDynamoDBAdapterClient(dynamoDBStreams);
|
||||
}
|
||||
else {
|
||||
this.dynamoDBStreamsAdapter = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setExtendedBindingProperties(KinesisExtendedBindingProperties extendedBindingProperties) {
|
||||
this.extendedBindingProperties = extendedBindingProperties;
|
||||
}
|
||||
|
||||
public void setCheckpointStore(ConcurrentMetadataStore checkpointStore) {
|
||||
this.checkpointStore = checkpointStore;
|
||||
}
|
||||
|
||||
public void setLockRegistry(LockRegistry lockRegistry) {
|
||||
this.lockRegistry = lockRegistry;
|
||||
}
|
||||
|
||||
public void setKinesisProducerConfiguration(KinesisProducerConfiguration kinesisProducerConfiguration) {
|
||||
this.kinesisProducerConfiguration = kinesisProducerConfiguration;
|
||||
}
|
||||
|
||||
public void setKinesisClientLibConfigurations(List<KinesisClientLibConfiguration> kinesisClientLibConfigurations) {
|
||||
this.kinesisClientLibConfigurations = kinesisClientLibConfigurations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KinesisConsumerProperties getExtendedConsumerProperties(String channelName) {
|
||||
return this.extendedBindingProperties.getExtendedConsumerProperties(channelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KinesisProducerProperties getExtendedProducerProperties(String channelName) {
|
||||
return this.extendedBindingProperties.getExtendedProducerProperties(channelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultsPrefix() {
|
||||
return this.extendedBindingProperties.getDefaultsPrefix();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends BinderSpecificPropertiesProvider> getExtendedPropertiesEntryClass() {
|
||||
return this.extendedBindingProperties.getExtendedPropertiesEntryClass();
|
||||
}
|
||||
|
||||
public AmazonKinesisAsync getAmazonKinesis() {
|
||||
return this.amazonKinesis;
|
||||
}
|
||||
|
||||
public List<String> getStreamsInUse() {
|
||||
return this.streamsInUse;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInit() throws Exception {
|
||||
super.onInit();
|
||||
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageHandler createProducerMessageHandler(ProducerDestination destination,
|
||||
ExtendedProducerProperties<KinesisProducerProperties> producerProperties,
|
||||
MessageChannel errorChannel) {
|
||||
|
||||
FunctionExpression<Message<?>> partitionKeyExpression =
|
||||
new FunctionExpression<>((m) ->
|
||||
m.getHeaders().containsKey(BinderHeaders.PARTITION_HEADER)
|
||||
? m.getHeaders().get(BinderHeaders.PARTITION_HEADER)
|
||||
: m.getPayload().hashCode());
|
||||
final AbstractAwsMessageHandler<?> messageHandler;
|
||||
if (this.configurationProperties.isKplKclEnabled()) {
|
||||
messageHandler = createKplMessageHandler(destination, partitionKeyExpression);
|
||||
}
|
||||
else {
|
||||
messageHandler = createKinesisMessageHandler(destination, partitionKeyExpression);
|
||||
}
|
||||
messageHandler.setSync(producerProperties.getExtension().isSync());
|
||||
messageHandler.setSendTimeout(producerProperties.getExtension().getSendTimeout());
|
||||
messageHandler.setFailureChannel(errorChannel);
|
||||
messageHandler.setBeanFactory(getBeanFactory());
|
||||
|
||||
this.streamsInUse.add(destination.getName());
|
||||
|
||||
return messageHandler;
|
||||
}
|
||||
|
||||
private AbstractAwsMessageHandler<?> createKinesisMessageHandler(ProducerDestination destination,
|
||||
FunctionExpression<Message<?>> partitionKeyExpression) {
|
||||
|
||||
final KinesisMessageHandler messageHandler;
|
||||
messageHandler = new KinesisMessageHandler(this.amazonKinesis);
|
||||
messageHandler.setStream(destination.getName());
|
||||
messageHandler.setPartitionKeyExpression(partitionKeyExpression);
|
||||
return messageHandler;
|
||||
}
|
||||
|
||||
private AbstractAwsMessageHandler<?> createKplMessageHandler(ProducerDestination destination,
|
||||
FunctionExpression<Message<?>> partitionKeyExpression) {
|
||||
|
||||
final KplMessageHandler messageHandler;
|
||||
messageHandler = new KplMessageHandler(new KinesisProducer(this.kinesisProducerConfiguration));
|
||||
messageHandler.setStream(destination.getName());
|
||||
messageHandler.setPartitionKeyExpression(partitionKeyExpression);
|
||||
return messageHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void postProcessOutputChannel(MessageChannel outputChannel,
|
||||
ExtendedProducerProperties<KinesisProducerProperties> producerProperties) {
|
||||
|
||||
if (outputChannel instanceof InterceptableChannel && producerProperties.isPartitioned()) {
|
||||
((InterceptableChannel) outputChannel)
|
||||
.addInterceptor(0,
|
||||
new ChannelInterceptor() {
|
||||
|
||||
private final PartitionKeyExtractorStrategy partitionKeyExtractorStrategy;
|
||||
|
||||
{
|
||||
if (StringUtils.hasText(producerProperties.getPartitionKeyExtractorName())) {
|
||||
this.partitionKeyExtractorStrategy =
|
||||
getBeanFactory()
|
||||
.getBean(producerProperties.getPartitionKeyExtractorName(),
|
||||
PartitionKeyExtractorStrategy.class);
|
||||
}
|
||||
else {
|
||||
this.partitionKeyExtractorStrategy =
|
||||
(message) ->
|
||||
producerProperties.getPartitionKeyExpression()
|
||||
.getValue(KinesisMessageChannelBinder.this.evaluationContext,
|
||||
message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
Object partitionKey = this.partitionKeyExtractorStrategy.extractKey(message);
|
||||
return MessageBuilder.fromMessage(message)
|
||||
.setHeader(BinderHeaders.PARTITION_OVERRIDE, partitionKey)
|
||||
.build();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageProducer createConsumerEndpoint(ConsumerDestination destination,
|
||||
String group,
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> properties) {
|
||||
|
||||
ConsumerDestination destinationToUse = destination;
|
||||
|
||||
if (properties.getExtension().isDynamoDbStreams()) {
|
||||
DescribeTableResult describeTableResult = this.dynamoDBClient.describeTable(destinationToUse.getName());
|
||||
String latestStreamArn = describeTableResult.getTable().getLatestStreamArn();
|
||||
if (StringUtils.hasText(latestStreamArn)) {
|
||||
destinationToUse = new KinesisConsumerDestination(latestStreamArn, Collections.emptyList());
|
||||
}
|
||||
else {
|
||||
throw new ProvisioningException("The DynamoDB table ["
|
||||
+ destinationToUse.getName()
|
||||
+ "] doesn't have Streams enabled.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.streamsInUse.add(destinationToUse.getName());
|
||||
}
|
||||
|
||||
MessageProducer adapter;
|
||||
if (this.configurationProperties.isKplKclEnabled()) {
|
||||
adapter = createKclConsumerEndpoint(destinationToUse, group, properties);
|
||||
}
|
||||
else {
|
||||
adapter = createKinesisConsumerEndpoint(destinationToUse, group, properties);
|
||||
}
|
||||
|
||||
return adapter;
|
||||
}
|
||||
|
||||
private MessageProducer createKclConsumerEndpoint(ConsumerDestination destination, String group,
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> properties) {
|
||||
KinesisConsumerProperties kinesisConsumerProperties = properties.getExtension();
|
||||
|
||||
if (kinesisConsumerProperties.getShardId() != null) {
|
||||
logger.warn("Kinesis Client Library doesn't does not support explicit shard configuration. " +
|
||||
"Ignoring 'shardId' property");
|
||||
}
|
||||
|
||||
String shardIteratorType = kinesisConsumerProperties.getShardIteratorType();
|
||||
|
||||
AmazonKinesis amazonKinesisClient =
|
||||
kinesisConsumerProperties.isDynamoDbStreams()
|
||||
? this.dynamoDBStreamsAdapter
|
||||
: this.amazonKinesis;
|
||||
|
||||
String stream = destination.getName();
|
||||
|
||||
KinesisClientLibConfiguration kinesisClientLibConfiguration = obtainKinesisClientLibConfiguration(stream, group);
|
||||
|
||||
KclMessageDrivenChannelAdapter adapter;
|
||||
|
||||
String consumerGroup;
|
||||
if (kinesisClientLibConfiguration == null) {
|
||||
adapter = new KclMessageDrivenChannelAdapter(stream, amazonKinesisClient, this.cloudWatchClient,
|
||||
this.dynamoDBClient, this.awsCredentialsProvider);
|
||||
boolean anonymous = !StringUtils.hasText(group);
|
||||
consumerGroup = anonymous ? "anonymous." + UUID.randomUUID() : group;
|
||||
adapter.setConsumerGroup(consumerGroup);
|
||||
if (StringUtils.hasText(shardIteratorType)) {
|
||||
adapter.setStreamInitialSequence(InitialPositionInStream.valueOf(shardIteratorType));
|
||||
}
|
||||
adapter.setIdleBetweenPolls(kinesisConsumerProperties.getIdleBetweenPolls());
|
||||
adapter.setConsumerBackoff(kinesisConsumerProperties.getConsumerBackoff());
|
||||
if (kinesisConsumerProperties.getWorkerId() != null) {
|
||||
adapter.setWorkerId(kinesisConsumerProperties.getWorkerId());
|
||||
}
|
||||
}
|
||||
else {
|
||||
adapter = new KclMessageDrivenChannelAdapter(kinesisClientLibConfiguration, amazonKinesisClient,
|
||||
this.cloudWatchClient, this.dynamoDBClient);
|
||||
consumerGroup = kinesisClientLibConfiguration.getApplicationName();
|
||||
}
|
||||
|
||||
adapter.setCheckpointMode(kinesisConsumerProperties.getCheckpointMode());
|
||||
adapter.setCheckpointsInterval(kinesisConsumerProperties.getCheckpointInterval());
|
||||
|
||||
adapter.setListenerMode(kinesisConsumerProperties.getListenerMode());
|
||||
|
||||
if (properties.isUseNativeDecoding()) {
|
||||
adapter.setConverter(null);
|
||||
}
|
||||
else {
|
||||
// Defer byte[] conversion to the InboundContentTypeConvertingInterceptor
|
||||
adapter.setConverter((bytes) -> bytes);
|
||||
}
|
||||
|
||||
ErrorInfrastructure errorInfrastructure = registerErrorInfrastructure(destination, consumerGroup, properties);
|
||||
adapter.setErrorMessageStrategy(ERROR_MESSAGE_STRATEGY);
|
||||
adapter.setErrorChannel(errorInfrastructure.getErrorChannel());
|
||||
adapter.setBindSourceRecord(true);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
private KinesisClientLibConfiguration obtainKinesisClientLibConfiguration(String stream, String group) {
|
||||
KinesisClientLibConfiguration candidate = null;
|
||||
for (KinesisClientLibConfiguration conf : this.kinesisClientLibConfigurations) {
|
||||
if (stream.equals(conf.getStreamName())) {
|
||||
candidate = conf;
|
||||
if (Objects.equals(group, conf.getApplicationName())) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private MessageProducer createKinesisConsumerEndpoint(ConsumerDestination destination, String group,
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> properties) {
|
||||
|
||||
KinesisConsumerProperties kinesisConsumerProperties = properties.getExtension();
|
||||
|
||||
if (properties.getInstanceCount() > 1 && properties.getExtension().getShardId() != null) {
|
||||
throw new InvalidArgumentException("'instanceCount' more than 1 and 'shardId' cannot be provided together.");
|
||||
}
|
||||
|
||||
Set<KinesisShardOffset> shardOffsets = null;
|
||||
|
||||
String shardIteratorType = kinesisConsumerProperties.getShardIteratorType();
|
||||
|
||||
KinesisShardOffset kinesisShardOffset = KinesisShardOffset.latest();
|
||||
|
||||
if (StringUtils.hasText(shardIteratorType)) {
|
||||
String[] typeValue = shardIteratorType.split(":", 2);
|
||||
ShardIteratorType iteratorType = ShardIteratorType.valueOf(typeValue[0]);
|
||||
kinesisShardOffset = new KinesisShardOffset(iteratorType);
|
||||
if (typeValue.length > 1) {
|
||||
if (ShardIteratorType.AT_TIMESTAMP.equals(iteratorType)) {
|
||||
kinesisShardOffset
|
||||
.setTimestamp(new Date(Long.parseLong(typeValue[1])));
|
||||
}
|
||||
else {
|
||||
kinesisShardOffset.setSequenceNumber(typeValue[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (properties.getInstanceCount() > 1) {
|
||||
shardOffsets = new HashSet<>();
|
||||
KinesisConsumerDestination kinesisConsumerDestination = (KinesisConsumerDestination) destination;
|
||||
List<Shard> shards = kinesisConsumerDestination.getShards();
|
||||
for (int i = 0; i < shards.size(); i++) {
|
||||
// divide shards across instances
|
||||
if ((i % properties.getInstanceCount()) == properties.getInstanceIndex()) {
|
||||
KinesisShardOffset shardOffset = new KinesisShardOffset(kinesisShardOffset);
|
||||
shardOffset.setStream(destination.getName());
|
||||
shardOffset.setShard(shards.get(i).getShardId());
|
||||
shardOffsets.add(shardOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KinesisMessageDrivenChannelAdapter adapter;
|
||||
|
||||
AmazonKinesis amazonKinesisClient =
|
||||
kinesisConsumerProperties.isDynamoDbStreams()
|
||||
? this.dynamoDBStreamsAdapter
|
||||
: this.amazonKinesis;
|
||||
|
||||
String shardId = kinesisConsumerProperties.getShardId();
|
||||
|
||||
if (CollectionUtils.isEmpty(shardOffsets) && shardId == null) {
|
||||
adapter = new KinesisMessageDrivenChannelAdapter(amazonKinesisClient, destination.getName());
|
||||
}
|
||||
else if (shardId != null) {
|
||||
KinesisShardOffset shardOffset = new KinesisShardOffset(kinesisShardOffset);
|
||||
shardOffset.setStream(destination.getName());
|
||||
shardOffset.setShard(shardId);
|
||||
adapter = new KinesisMessageDrivenChannelAdapter(amazonKinesisClient, shardOffset);
|
||||
}
|
||||
else {
|
||||
adapter = new KinesisMessageDrivenChannelAdapter(amazonKinesisClient,
|
||||
shardOffsets.toArray(new KinesisShardOffset[0]));
|
||||
}
|
||||
|
||||
boolean anonymous = !StringUtils.hasText(group);
|
||||
String consumerGroup = anonymous ? "anonymous." + UUID.randomUUID() : group;
|
||||
adapter.setConsumerGroup(consumerGroup);
|
||||
|
||||
adapter.setStreamInitialSequence(
|
||||
anonymous || StringUtils.hasText(shardIteratorType) ? kinesisShardOffset
|
||||
: KinesisShardOffset.trimHorizon());
|
||||
|
||||
adapter.setListenerMode(kinesisConsumerProperties.getListenerMode());
|
||||
|
||||
if (properties.isUseNativeDecoding()) {
|
||||
adapter.setConverter(null);
|
||||
}
|
||||
else {
|
||||
// Defer byte[] conversion to the InboundContentTypeConvertingInterceptor
|
||||
adapter.setConverter((bytes) -> bytes);
|
||||
}
|
||||
|
||||
adapter.setCheckpointMode(kinesisConsumerProperties.getCheckpointMode());
|
||||
adapter.setRecordsLimit(kinesisConsumerProperties.getRecordsLimit());
|
||||
adapter.setIdleBetweenPolls(kinesisConsumerProperties.getIdleBetweenPolls());
|
||||
adapter.setConsumerBackoff(kinesisConsumerProperties.getConsumerBackoff());
|
||||
adapter.setCheckpointsInterval(kinesisConsumerProperties.getCheckpointInterval());
|
||||
|
||||
if (this.checkpointStore != null) {
|
||||
adapter.setCheckpointStore(this.checkpointStore);
|
||||
}
|
||||
|
||||
adapter.setLockRegistry(this.lockRegistry);
|
||||
|
||||
adapter.setConcurrency(properties.getConcurrency());
|
||||
adapter.setStartTimeout(kinesisConsumerProperties.getStartTimeout());
|
||||
adapter.setDescribeStreamBackoff(
|
||||
this.configurationProperties.getDescribeStreamBackoff());
|
||||
adapter.setDescribeStreamRetries(
|
||||
this.configurationProperties.getDescribeStreamRetries());
|
||||
|
||||
ErrorInfrastructure errorInfrastructure = registerErrorInfrastructure(destination,
|
||||
consumerGroup, properties);
|
||||
adapter.setErrorMessageStrategy(ERROR_MESSAGE_STRATEGY);
|
||||
adapter.setErrorChannel(errorInfrastructure.getErrorChannel());
|
||||
adapter.setBindSourceRecord(true);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ErrorMessageStrategy getErrorMessageStrategy() {
|
||||
return ERROR_MESSAGE_STRATEGY;
|
||||
}
|
||||
|
||||
private static String[] headersToMap(KinesisBinderConfigurationProperties configurationProperties) {
|
||||
Assert.notNull(configurationProperties,
|
||||
"'configurationProperties' must not be null");
|
||||
if (ObjectUtils.isEmpty(configurationProperties.getHeaders())) {
|
||||
return BinderHeaders.STANDARD_HEADERS;
|
||||
}
|
||||
else {
|
||||
String[] combinedHeadersToMap = Arrays.copyOfRange(
|
||||
BinderHeaders.STANDARD_HEADERS, 0,
|
||||
BinderHeaders.STANDARD_HEADERS.length
|
||||
+ configurationProperties.getHeaders().length);
|
||||
System.arraycopy(configurationProperties.getHeaders(), 0,
|
||||
combinedHeadersToMap, BinderHeaders.STANDARD_HEADERS.length,
|
||||
configurationProperties.getHeaders().length);
|
||||
return combinedHeadersToMap;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2020-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis.adapter;
|
||||
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBStreams;
|
||||
import com.amazonaws.services.dynamodbv2.model.AmazonDynamoDBException;
|
||||
import com.amazonaws.services.dynamodbv2.streamsadapter.AmazonDynamoDBStreamsAdapterClient;
|
||||
import com.amazonaws.services.kinesis.model.DescribeStreamRequest;
|
||||
import com.amazonaws.services.kinesis.model.InvalidArgumentException;
|
||||
import com.amazonaws.services.kinesis.model.ListShardsRequest;
|
||||
import com.amazonaws.services.kinesis.model.ListShardsResult;
|
||||
import com.amazonaws.services.kinesis.model.ResourceNotFoundException;
|
||||
import com.amazonaws.services.kinesis.model.StreamDescription;
|
||||
|
||||
/**
|
||||
* This is Spring Cloud DynamoDB Adapter to be able to support {@code ListShards} operations.
|
||||
*
|
||||
* @author Asiel Caballero
|
||||
*
|
||||
* @since 2.0.3
|
||||
*
|
||||
* @see <a href="https://docs.aws.amazon.com/kinesis/latest/APIReference/API_ListShards.html">ListShards</a>
|
||||
*/
|
||||
public class SpringDynamoDBAdapterClient extends AmazonDynamoDBStreamsAdapterClient {
|
||||
|
||||
private static final String SEPARATOR = "!!##%%";
|
||||
|
||||
public SpringDynamoDBAdapterClient(AmazonDynamoDBStreams amazonDynamoDBStreams) {
|
||||
super(amazonDynamoDBStreams);
|
||||
}
|
||||
|
||||
/**
|
||||
* List shards for a DynamoDB Stream using its {@code DescribeStream} API, as they don't support
|
||||
* {@code ListShards} operations. Returns the result adapted to use the AmazonKinesis model.
|
||||
* @param request Container for the necessary parameters to execute the ListShards service method
|
||||
* @return The response from the DescribeStream service method, adapted for use with the AmazonKinesis model
|
||||
*/
|
||||
@Override
|
||||
public ListShardsResult listShards(ListShardsRequest request) {
|
||||
try {
|
||||
if (request.getNextToken() != null && request.getStreamName() != null) {
|
||||
throw new InvalidArgumentException("NextToken and StreamName cannot be provided together.");
|
||||
}
|
||||
|
||||
String streamName = request.getStreamName();
|
||||
String exclusiveStartShardId = request.getExclusiveStartShardId();
|
||||
|
||||
if (request.getNextToken() != null) {
|
||||
String[] split = request.getNextToken().split(SEPARATOR);
|
||||
|
||||
if (split.length != 2) {
|
||||
throw new InvalidArgumentException("Invalid ShardIterator");
|
||||
}
|
||||
|
||||
streamName = split[0];
|
||||
exclusiveStartShardId = split[1];
|
||||
}
|
||||
|
||||
DescribeStreamRequest dsr = new DescribeStreamRequest()
|
||||
.withStreamName(streamName)
|
||||
.withExclusiveStartShardId(exclusiveStartShardId)
|
||||
.withLimit(request.getMaxResults());
|
||||
StreamDescription streamDescription = describeStream(dsr).getStreamDescription();
|
||||
|
||||
ListShardsResult result = new ListShardsResult()
|
||||
.withShards(streamDescription.getShards());
|
||||
|
||||
if (streamDescription.getHasMoreShards()) {
|
||||
result.withNextToken(buildFakeNextToken(streamName,
|
||||
streamDescription.getShards().get(streamDescription.getShards().size() - 1).getShardId()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (AmazonDynamoDBException ex) {
|
||||
ResourceNotFoundException resourceEx = new ResourceNotFoundException(ex.getMessage());
|
||||
resourceEx.setStackTrace(ex.getStackTrace());
|
||||
throw resourceEx;
|
||||
}
|
||||
}
|
||||
|
||||
private String buildFakeNextToken(String streamName, String lastShard) {
|
||||
return lastShard != null ? streamName + SEPARATOR + lastShard : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* Copyright 2017-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis.config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.amazonaws.auth.AWSCredentialsProvider;
|
||||
import com.amazonaws.services.cloudwatch.AmazonCloudWatch;
|
||||
import com.amazonaws.services.cloudwatch.AmazonCloudWatchAsync;
|
||||
import com.amazonaws.services.cloudwatch.AmazonCloudWatchAsyncClientBuilder;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBAsync;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBAsyncClientBuilder;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBStreams;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBStreamsClientBuilder;
|
||||
import com.amazonaws.services.kinesis.AmazonKinesisAsync;
|
||||
import com.amazonaws.services.kinesis.AmazonKinesisAsyncClientBuilder;
|
||||
import com.amazonaws.services.kinesis.clientlibrary.lib.worker.KinesisClientLibConfiguration;
|
||||
import com.amazonaws.services.kinesis.producer.KinesisProducerConfiguration;
|
||||
import io.awspring.cloud.autoconfigure.context.ContextCredentialsAutoConfiguration;
|
||||
import io.awspring.cloud.autoconfigure.context.ContextRegionProviderAutoConfiguration;
|
||||
import io.awspring.cloud.core.region.RegionProvider;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.stream.binder.Binder;
|
||||
import org.springframework.cloud.stream.binder.kinesis.KinesisBinderHealthIndicator;
|
||||
import org.springframework.cloud.stream.binder.kinesis.KinesisMessageChannelBinder;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisBinderConfigurationProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisExtendedBindingProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.provisioning.KinesisStreamProvisioner;
|
||||
import org.springframework.cloud.stream.binding.Bindable;
|
||||
import org.springframework.cloud.stream.config.ConsumerEndpointCustomizer;
|
||||
import org.springframework.cloud.stream.config.ProducerMessageHandlerCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.integration.aws.lock.DynamoDbLockRegistry;
|
||||
import org.springframework.integration.aws.metadata.DynamoDbMetadataStore;
|
||||
import org.springframework.integration.aws.outbound.AbstractAwsMessageHandler;
|
||||
import org.springframework.integration.endpoint.MessageProducerSupport;
|
||||
import org.springframework.integration.metadata.ConcurrentMetadataStore;
|
||||
import org.springframework.integration.support.locks.LockRegistry;
|
||||
|
||||
/**
|
||||
* The auto-configuration for AWS components and Spring Cloud Stream Kinesis Binder.
|
||||
*
|
||||
* @author Peter Oates
|
||||
* @author Artem Bilan
|
||||
* @author Arnaud Lecollaire
|
||||
* @author Asiel Caballero
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnMissingBean(Binder.class)
|
||||
@EnableConfigurationProperties({ KinesisBinderConfigurationProperties.class, KinesisExtendedBindingProperties.class })
|
||||
@Import({ ContextCredentialsAutoConfiguration.class, ContextRegionProviderAutoConfiguration.class })
|
||||
public class KinesisBinderConfiguration {
|
||||
|
||||
private final KinesisBinderConfigurationProperties configurationProperties;
|
||||
|
||||
private final AWSCredentialsProvider awsCredentialsProvider;
|
||||
|
||||
private final String region;
|
||||
|
||||
private final boolean hasInputs;
|
||||
|
||||
public KinesisBinderConfiguration(KinesisBinderConfigurationProperties configurationProperties,
|
||||
AWSCredentialsProvider awsCredentialsProvider,
|
||||
RegionProvider regionProvider,
|
||||
List<Bindable> bindables) {
|
||||
|
||||
this.configurationProperties = configurationProperties;
|
||||
this.awsCredentialsProvider = awsCredentialsProvider;
|
||||
this.region = regionProvider.getRegion().getName();
|
||||
this.hasInputs =
|
||||
bindables.stream()
|
||||
.map(Bindable::getInputs)
|
||||
.flatMap(Set::stream)
|
||||
.findFirst()
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public AmazonKinesisAsync amazonKinesis() {
|
||||
return AmazonKinesisAsyncClientBuilder.standard()
|
||||
.withCredentials(this.awsCredentialsProvider)
|
||||
.withRegion(this.region)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KinesisStreamProvisioner provisioningProvider(AmazonKinesisAsync amazonKinesis) {
|
||||
return new KinesisStreamProvisioner(amazonKinesis, this.configurationProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public AmazonDynamoDBAsync dynamoDB() {
|
||||
if (this.hasInputs) {
|
||||
return AmazonDynamoDBAsyncClientBuilder.standard()
|
||||
.withCredentials(this.awsCredentialsProvider)
|
||||
.withRegion(this.region)
|
||||
.build();
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnBean(AmazonDynamoDBAsync.class)
|
||||
@ConditionalOnProperty(name = "spring.cloud.stream.kinesis.binder.kpl-kcl-enabled", havingValue = "false",
|
||||
matchIfMissing = true)
|
||||
public LockRegistry dynamoDBLockRegistry(@Autowired(required = false) AmazonDynamoDBAsync dynamoDB) {
|
||||
if (dynamoDB != null) {
|
||||
KinesisBinderConfigurationProperties.Locks locks = this.configurationProperties.getLocks();
|
||||
DynamoDbLockRegistry dynamoDbLockRegistry = new DynamoDbLockRegistry(dynamoDB, locks.getTable());
|
||||
dynamoDbLockRegistry.setRefreshPeriod(locks.getRefreshPeriod());
|
||||
dynamoDbLockRegistry.setHeartbeatPeriod(locks.getHeartbeatPeriod());
|
||||
dynamoDbLockRegistry.setLeaseDuration(locks.getLeaseDuration());
|
||||
dynamoDbLockRegistry.setPartitionKey(locks.getPartitionKey());
|
||||
dynamoDbLockRegistry.setSortKeyName(locks.getSortKeyName());
|
||||
dynamoDbLockRegistry.setSortKey(locks.getSortKey());
|
||||
dynamoDbLockRegistry.setBillingMode(locks.getBillingMode());
|
||||
dynamoDbLockRegistry.setReadCapacity(locks.getReadCapacity());
|
||||
dynamoDbLockRegistry.setWriteCapacity(locks.getWriteCapacity());
|
||||
return dynamoDbLockRegistry;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnBean(AmazonDynamoDBAsync.class)
|
||||
@ConditionalOnProperty(name = "spring.cloud.stream.kinesis.binder.kpl-kcl-enabled", havingValue = "false",
|
||||
matchIfMissing = true)
|
||||
public ConcurrentMetadataStore kinesisCheckpointStore(@Autowired(required = false) AmazonDynamoDBAsync dynamoDB) {
|
||||
if (dynamoDB != null) {
|
||||
KinesisBinderConfigurationProperties.Checkpoint checkpoint = this.configurationProperties.getCheckpoint();
|
||||
DynamoDbMetadataStore kinesisCheckpointStore = new DynamoDbMetadataStore(dynamoDB, checkpoint.getTable());
|
||||
kinesisCheckpointStore.setBillingMode(checkpoint.getBillingMode());
|
||||
kinesisCheckpointStore.setReadCapacity(checkpoint.getReadCapacity());
|
||||
kinesisCheckpointStore.setWriteCapacity(checkpoint.getWriteCapacity());
|
||||
kinesisCheckpointStore.setCreateTableDelay(checkpoint.getCreateDelay());
|
||||
kinesisCheckpointStore.setCreateTableRetries(checkpoint.getCreateRetries());
|
||||
if (checkpoint.getTimeToLive() != null) {
|
||||
kinesisCheckpointStore.setTimeToLive(checkpoint.getTimeToLive());
|
||||
}
|
||||
return kinesisCheckpointStore;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public AmazonDynamoDBStreams dynamoDBStreams() {
|
||||
if (this.hasInputs) {
|
||||
return AmazonDynamoDBStreamsClientBuilder.standard()
|
||||
.withCredentials(this.awsCredentialsProvider)
|
||||
.withRegion(this.region)
|
||||
.build();
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(name = "spring.cloud.stream.kinesis.binder.kpl-kcl-enabled")
|
||||
public AmazonCloudWatchAsync cloudWatch() {
|
||||
if (this.hasInputs) {
|
||||
return AmazonCloudWatchAsyncClientBuilder.standard()
|
||||
.withCredentials(this.awsCredentialsProvider)
|
||||
.withRegion(this.region)
|
||||
.build();
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(name = "spring.cloud.stream.kinesis.binder.kpl-kcl-enabled")
|
||||
public KinesisProducerConfiguration kinesisProducerConfiguration() {
|
||||
KinesisProducerConfiguration kinesisProducerConfiguration = new KinesisProducerConfiguration();
|
||||
kinesisProducerConfiguration.setCredentialsProvider(this.awsCredentialsProvider);
|
||||
kinesisProducerConfiguration.setRegion(this.region);
|
||||
return kinesisProducerConfiguration;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KinesisMessageChannelBinder kinesisMessageChannelBinder(
|
||||
KinesisStreamProvisioner provisioningProvider,
|
||||
AmazonKinesisAsync amazonKinesis,
|
||||
KinesisExtendedBindingProperties kinesisExtendedBindingProperties,
|
||||
@Autowired(required = false) ConcurrentMetadataStore kinesisCheckpointStore,
|
||||
@Autowired(required = false) LockRegistry lockRegistry,
|
||||
@Autowired(required = false) AmazonDynamoDB dynamoDBClient,
|
||||
@Autowired(required = false) AmazonDynamoDBStreams dynamoDBStreams,
|
||||
@Autowired(required = false) AmazonCloudWatch cloudWatchClient,
|
||||
@Autowired(required = false) KinesisProducerConfiguration kinesisProducerConfiguration,
|
||||
@Autowired(required = false) ProducerMessageHandlerCustomizer<? extends AbstractAwsMessageHandler<Void>> producerMessageHandlerCustomizer,
|
||||
@Autowired(required = false) ConsumerEndpointCustomizer<? extends MessageProducerSupport> consumerEndpointCustomizer,
|
||||
@Autowired List<KinesisClientLibConfiguration> kinesisClientLibConfigurations) {
|
||||
|
||||
KinesisMessageChannelBinder kinesisMessageChannelBinder =
|
||||
new KinesisMessageChannelBinder(this.configurationProperties, provisioningProvider, amazonKinesis,
|
||||
this.awsCredentialsProvider, dynamoDBClient, dynamoDBStreams, cloudWatchClient);
|
||||
kinesisMessageChannelBinder.setCheckpointStore(kinesisCheckpointStore);
|
||||
kinesisMessageChannelBinder.setLockRegistry(lockRegistry);
|
||||
kinesisMessageChannelBinder.setExtendedBindingProperties(kinesisExtendedBindingProperties);
|
||||
kinesisMessageChannelBinder.setKinesisProducerConfiguration(kinesisProducerConfiguration);
|
||||
kinesisMessageChannelBinder.setProducerMessageHandlerCustomizer(producerMessageHandlerCustomizer);
|
||||
kinesisMessageChannelBinder.setConsumerEndpointCustomizer(consumerEndpointCustomizer);
|
||||
kinesisMessageChannelBinder.setKinesisClientLibConfigurations(kinesisClientLibConfigurations);
|
||||
return kinesisMessageChannelBinder;
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(HealthIndicator.class)
|
||||
@ConditionalOnEnabledHealthIndicator("binders")
|
||||
protected static class KinesisBinderHealthIndicatorConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "kinesisBinderHealthIndicator")
|
||||
public KinesisBinderHealthIndicator kinesisBinderHealthIndicator(
|
||||
KinesisMessageChannelBinder kinesisMessageChannelBinder) {
|
||||
|
||||
return new KinesisBinderHealthIndicator(kinesisMessageChannelBinder);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* Copyright 2017-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis.properties;
|
||||
|
||||
import com.amazonaws.services.dynamodbv2.model.BillingMode;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.integration.aws.lock.DynamoDbLockRegistry;
|
||||
import org.springframework.integration.aws.metadata.DynamoDbMetadataStore;
|
||||
|
||||
/**
|
||||
* The Kinesis Binder specific configuration properties.
|
||||
*
|
||||
* @author Peter Oates
|
||||
* @author Artem Bilan
|
||||
* @author Jacob Severson
|
||||
* @author Sergiu Pantiru
|
||||
* @author Arnaud Lecollaire
|
||||
* @author Asiel Caballero
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "spring.cloud.stream.kinesis.binder")
|
||||
public class KinesisBinderConfigurationProperties {
|
||||
|
||||
private String[] headers = new String[] { };
|
||||
|
||||
private int describeStreamBackoff = 1000;
|
||||
|
||||
private int describeStreamRetries = 50;
|
||||
|
||||
private boolean autoCreateStream = true;
|
||||
|
||||
private boolean autoAddShards = false;
|
||||
|
||||
private int minShardCount = 1;
|
||||
|
||||
/**
|
||||
* Enables the usage of Amazon KCL/KPL libraries for all message consumption and production.
|
||||
*/
|
||||
private boolean kplKclEnabled;
|
||||
|
||||
private final Checkpoint checkpoint = new Checkpoint();
|
||||
|
||||
private final Locks locks = new Locks();
|
||||
|
||||
public String[] getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
public void setHeaders(String... headers) {
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
public int getDescribeStreamBackoff() {
|
||||
return this.describeStreamBackoff;
|
||||
}
|
||||
|
||||
public void setDescribeStreamBackoff(int describeStreamBackoff) {
|
||||
this.describeStreamBackoff = describeStreamBackoff;
|
||||
}
|
||||
|
||||
public int getDescribeStreamRetries() {
|
||||
return this.describeStreamRetries;
|
||||
}
|
||||
|
||||
public void setDescribeStreamRetries(int describeStreamRetries) {
|
||||
this.describeStreamRetries = describeStreamRetries;
|
||||
}
|
||||
|
||||
public boolean isAutoAddShards() {
|
||||
return this.autoAddShards;
|
||||
}
|
||||
|
||||
public void setAutoAddShards(boolean autoAddShards) {
|
||||
this.autoAddShards = autoAddShards;
|
||||
}
|
||||
|
||||
public int getMinShardCount() {
|
||||
return this.minShardCount;
|
||||
}
|
||||
|
||||
public void setMinShardCount(int minShardCount) {
|
||||
this.minShardCount = minShardCount;
|
||||
}
|
||||
|
||||
public Checkpoint getCheckpoint() {
|
||||
return this.checkpoint;
|
||||
}
|
||||
|
||||
public Locks getLocks() {
|
||||
return this.locks;
|
||||
}
|
||||
|
||||
public boolean isAutoCreateStream() {
|
||||
return this.autoCreateStream;
|
||||
}
|
||||
|
||||
public void setAutoCreateStream(boolean autoCreateStream) {
|
||||
this.autoCreateStream = autoCreateStream;
|
||||
}
|
||||
|
||||
public boolean isKplKclEnabled() {
|
||||
return this.kplKclEnabled;
|
||||
}
|
||||
|
||||
public void setKplKclEnabled(boolean kplKclEnabled) {
|
||||
this.kplKclEnabled = kplKclEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* The checkpoint DynamoDB table configuration properties.
|
||||
*/
|
||||
public static class Checkpoint {
|
||||
|
||||
private String table = DynamoDbMetadataStore.DEFAULT_TABLE_NAME;
|
||||
|
||||
private BillingMode billingMode = BillingMode.PAY_PER_REQUEST;
|
||||
|
||||
private long readCapacity = 1L;
|
||||
|
||||
private long writeCapacity = 1L;
|
||||
|
||||
private int createDelay = 1;
|
||||
|
||||
private int createRetries = 25;
|
||||
|
||||
private Integer timeToLive;
|
||||
|
||||
public String getTable() {
|
||||
return this.table;
|
||||
}
|
||||
|
||||
public void setTable(String table) {
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
public BillingMode getBillingMode() {
|
||||
return billingMode;
|
||||
}
|
||||
|
||||
public void setBillingMode(BillingMode billingMode) {
|
||||
this.billingMode = billingMode;
|
||||
}
|
||||
|
||||
public long getReadCapacity() {
|
||||
return this.readCapacity;
|
||||
}
|
||||
|
||||
public void setReadCapacity(long readCapacity) {
|
||||
this.readCapacity = readCapacity;
|
||||
}
|
||||
|
||||
public long getWriteCapacity() {
|
||||
return this.writeCapacity;
|
||||
}
|
||||
|
||||
public void setWriteCapacity(long writeCapacity) {
|
||||
this.writeCapacity = writeCapacity;
|
||||
}
|
||||
|
||||
public int getCreateDelay() {
|
||||
return this.createDelay;
|
||||
}
|
||||
|
||||
public void setCreateDelay(int createDelay) {
|
||||
this.createDelay = createDelay;
|
||||
}
|
||||
|
||||
public int getCreateRetries() {
|
||||
return this.createRetries;
|
||||
}
|
||||
|
||||
public void setCreateRetries(int createRetries) {
|
||||
this.createRetries = createRetries;
|
||||
}
|
||||
|
||||
public Integer getTimeToLive() {
|
||||
return this.timeToLive;
|
||||
}
|
||||
|
||||
public void setTimeToLive(Integer timeToLive) {
|
||||
this.timeToLive = timeToLive;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The locks DynamoDB table configuration properties.
|
||||
*/
|
||||
public static class Locks {
|
||||
|
||||
private String table = DynamoDbLockRegistry.DEFAULT_TABLE_NAME;
|
||||
|
||||
private BillingMode billingMode = BillingMode.PAY_PER_REQUEST;
|
||||
|
||||
private long readCapacity = 1L;
|
||||
|
||||
private long writeCapacity = 1L;
|
||||
|
||||
private String partitionKey = DynamoDbLockRegistry.DEFAULT_PARTITION_KEY_NAME;
|
||||
|
||||
private String sortKeyName = DynamoDbLockRegistry.DEFAULT_SORT_KEY_NAME;
|
||||
|
||||
private String sortKey = DynamoDbLockRegistry.DEFAULT_SORT_KEY;
|
||||
|
||||
private long refreshPeriod = DynamoDbLockRegistry.DEFAULT_REFRESH_PERIOD_MS;
|
||||
|
||||
private long leaseDuration = 20L;
|
||||
|
||||
private long heartbeatPeriod = 5L;
|
||||
|
||||
public String getTable() {
|
||||
return this.table;
|
||||
}
|
||||
|
||||
public void setTable(String table) {
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
public BillingMode getBillingMode() {
|
||||
return billingMode;
|
||||
}
|
||||
|
||||
public void setBillingMode(BillingMode billingMode) {
|
||||
this.billingMode = billingMode;
|
||||
}
|
||||
|
||||
public long getReadCapacity() {
|
||||
return this.readCapacity;
|
||||
}
|
||||
|
||||
public void setReadCapacity(long readCapacity) {
|
||||
this.readCapacity = readCapacity;
|
||||
}
|
||||
|
||||
public long getWriteCapacity() {
|
||||
return this.writeCapacity;
|
||||
}
|
||||
|
||||
public void setWriteCapacity(long writeCapacity) {
|
||||
this.writeCapacity = writeCapacity;
|
||||
}
|
||||
|
||||
public String getPartitionKey() {
|
||||
return this.partitionKey;
|
||||
}
|
||||
|
||||
public void setPartitionKey(String partitionKey) {
|
||||
this.partitionKey = partitionKey;
|
||||
}
|
||||
|
||||
public String getSortKeyName() {
|
||||
return this.sortKeyName;
|
||||
}
|
||||
|
||||
public void setSortKeyName(String sortKeyName) {
|
||||
this.sortKeyName = sortKeyName;
|
||||
}
|
||||
|
||||
public String getSortKey() {
|
||||
return this.sortKey;
|
||||
}
|
||||
|
||||
public void setSortKey(String sortKey) {
|
||||
this.sortKey = sortKey;
|
||||
}
|
||||
|
||||
public long getRefreshPeriod() {
|
||||
return this.refreshPeriod;
|
||||
}
|
||||
|
||||
public void setRefreshPeriod(long refreshPeriod) {
|
||||
this.refreshPeriod = refreshPeriod;
|
||||
}
|
||||
|
||||
public long getLeaseDuration() {
|
||||
return this.leaseDuration;
|
||||
}
|
||||
|
||||
public void setLeaseDuration(long leaseDuration) {
|
||||
this.leaseDuration = leaseDuration;
|
||||
}
|
||||
|
||||
public long getHeartbeatPeriod() {
|
||||
return this.heartbeatPeriod;
|
||||
}
|
||||
|
||||
public void setHeartbeatPeriod(long heartbeatPeriod) {
|
||||
this.heartbeatPeriod = heartbeatPeriod;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis.properties;
|
||||
|
||||
import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider;
|
||||
|
||||
/**
|
||||
* The Kinesis-specific binding configuration properties.
|
||||
*
|
||||
* @author Peter Oates
|
||||
* @author Artem Bilan
|
||||
*/
|
||||
public class KinesisBindingProperties implements BinderSpecificPropertiesProvider {
|
||||
|
||||
private KinesisConsumerProperties consumer = new KinesisConsumerProperties();
|
||||
|
||||
private KinesisProducerProperties producer = new KinesisProducerProperties();
|
||||
|
||||
public KinesisConsumerProperties getConsumer() {
|
||||
return this.consumer;
|
||||
}
|
||||
|
||||
public void setConsumer(KinesisConsumerProperties consumer) {
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
public KinesisProducerProperties getProducer() {
|
||||
return this.producer;
|
||||
}
|
||||
|
||||
public void setProducer(KinesisProducerProperties producer) {
|
||||
this.producer = producer;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis.properties;
|
||||
|
||||
import org.springframework.integration.aws.inbound.kinesis.CheckpointMode;
|
||||
import org.springframework.integration.aws.inbound.kinesis.ListenerMode;
|
||||
|
||||
/**
|
||||
* The Kinesis-specific consumer binding configuration properties.
|
||||
*
|
||||
* @author Peter Oates
|
||||
* @author Jacob Severson
|
||||
* @author Artem Bilan
|
||||
* @author Arnaud Lecollaire
|
||||
* @author Dmytro Danilenkov
|
||||
*
|
||||
*/
|
||||
public class KinesisConsumerProperties {
|
||||
|
||||
private int startTimeout = 60000;
|
||||
|
||||
private ListenerMode listenerMode = ListenerMode.record;
|
||||
|
||||
private CheckpointMode checkpointMode = CheckpointMode.batch;
|
||||
|
||||
/**
|
||||
* Interval, in milliseconds, between two checkpoints when checkpoint mode is periodic.
|
||||
*/
|
||||
private Long checkpointInterval = 5_000L;
|
||||
|
||||
private int recordsLimit = 10000;
|
||||
|
||||
private int idleBetweenPolls = 1000;
|
||||
|
||||
private int consumerBackoff = 1000;
|
||||
|
||||
private String shardIteratorType;
|
||||
|
||||
private boolean dynamoDbStreams;
|
||||
|
||||
private String shardId;
|
||||
|
||||
/**
|
||||
* Worker identifier used to distinguish different workers/processes
|
||||
* (only used when KCL is enabled).
|
||||
*/
|
||||
private String workerId;
|
||||
|
||||
public int getStartTimeout() {
|
||||
return this.startTimeout;
|
||||
}
|
||||
|
||||
public void setStartTimeout(int startTimeout) {
|
||||
this.startTimeout = startTimeout;
|
||||
}
|
||||
|
||||
public ListenerMode getListenerMode() {
|
||||
return this.listenerMode;
|
||||
}
|
||||
|
||||
public void setListenerMode(ListenerMode listenerMode) {
|
||||
this.listenerMode = listenerMode;
|
||||
}
|
||||
|
||||
public CheckpointMode getCheckpointMode() {
|
||||
return this.checkpointMode;
|
||||
}
|
||||
|
||||
public void setCheckpointMode(CheckpointMode checkpointMode) {
|
||||
this.checkpointMode = checkpointMode;
|
||||
}
|
||||
|
||||
public Long getCheckpointInterval() {
|
||||
return checkpointInterval;
|
||||
}
|
||||
|
||||
public void setCheckpointInterval(Long checkpointInterval) {
|
||||
this.checkpointInterval = checkpointInterval;
|
||||
}
|
||||
|
||||
public int getRecordsLimit() {
|
||||
return this.recordsLimit;
|
||||
}
|
||||
|
||||
public void setRecordsLimit(int recordsLimit) {
|
||||
this.recordsLimit = recordsLimit;
|
||||
}
|
||||
|
||||
public int getIdleBetweenPolls() {
|
||||
return this.idleBetweenPolls;
|
||||
}
|
||||
|
||||
public void setIdleBetweenPolls(int idleBetweenPolls) {
|
||||
this.idleBetweenPolls = idleBetweenPolls;
|
||||
}
|
||||
|
||||
public int getConsumerBackoff() {
|
||||
return this.consumerBackoff;
|
||||
}
|
||||
|
||||
public void setConsumerBackoff(int consumerBackoff) {
|
||||
this.consumerBackoff = consumerBackoff;
|
||||
}
|
||||
|
||||
public String getShardIteratorType() {
|
||||
return this.shardIteratorType;
|
||||
}
|
||||
|
||||
public void setShardIteratorType(String shardIteratorType) {
|
||||
this.shardIteratorType = shardIteratorType;
|
||||
}
|
||||
|
||||
public String getWorkerId() {
|
||||
return workerId;
|
||||
}
|
||||
|
||||
public void setWorkerId(String workerId) {
|
||||
this.workerId = workerId;
|
||||
}
|
||||
|
||||
public boolean isDynamoDbStreams() {
|
||||
return this.dynamoDbStreams;
|
||||
}
|
||||
|
||||
public void setDynamoDbStreams(boolean dynamoDbStreams) {
|
||||
this.dynamoDbStreams = dynamoDbStreams;
|
||||
}
|
||||
|
||||
public String getShardId() {
|
||||
return shardId;
|
||||
}
|
||||
|
||||
public void setShardId(String shardId) {
|
||||
this.shardId = shardId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis.properties;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider;
|
||||
import org.springframework.cloud.stream.binder.ExtendedBindingProperties;
|
||||
|
||||
/**
|
||||
* The extended Kinesis-specific binding configuration properties.
|
||||
*
|
||||
* @author Peter Oates
|
||||
* @author Artem Bilan
|
||||
*
|
||||
*/
|
||||
@ConfigurationProperties("spring.cloud.stream.kinesis")
|
||||
public class KinesisExtendedBindingProperties implements
|
||||
ExtendedBindingProperties<KinesisConsumerProperties, KinesisProducerProperties> {
|
||||
|
||||
private static final String DEFAULTS_PREFIX = "spring.cloud.stream.kinesis.default";
|
||||
|
||||
private Map<String, KinesisBindingProperties> bindings = new HashMap<>();
|
||||
|
||||
public Map<String, KinesisBindingProperties> getBindings() {
|
||||
return this.bindings;
|
||||
}
|
||||
|
||||
public void setBindings(Map<String, KinesisBindingProperties> bindings) {
|
||||
this.bindings = bindings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KinesisConsumerProperties getExtendedConsumerProperties(String channelName) {
|
||||
if (this.bindings.containsKey(channelName)
|
||||
&& this.bindings.get(channelName).getConsumer() != null) {
|
||||
return this.bindings.get(channelName).getConsumer();
|
||||
}
|
||||
else {
|
||||
return new KinesisConsumerProperties();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public KinesisProducerProperties getExtendedProducerProperties(String channelName) {
|
||||
if (this.bindings.containsKey(channelName)
|
||||
&& this.bindings.get(channelName).getProducer() != null) {
|
||||
return this.bindings.get(channelName).getProducer();
|
||||
}
|
||||
else {
|
||||
return new KinesisProducerProperties();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultsPrefix() {
|
||||
return DEFAULTS_PREFIX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends BinderSpecificPropertiesProvider> getExtendedPropertiesEntryClass() {
|
||||
return KinesisBindingProperties.class;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis.properties;
|
||||
|
||||
/**
|
||||
* The Kinesis-specific producer binding configuration properties.
|
||||
*
|
||||
* @author Peter Oates
|
||||
* @author Jacob Severson
|
||||
*
|
||||
*/
|
||||
public class KinesisProducerProperties {
|
||||
|
||||
private boolean sync;
|
||||
|
||||
private long sendTimeout = 10000;
|
||||
|
||||
public void setSync(boolean sync) {
|
||||
this.sync = sync;
|
||||
}
|
||||
|
||||
public boolean isSync() {
|
||||
return this.sync;
|
||||
}
|
||||
|
||||
public long getSendTimeout() {
|
||||
return this.sendTimeout;
|
||||
}
|
||||
|
||||
public void setSendTimeout(long sendTimeout) {
|
||||
this.sendTimeout = sendTimeout;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis.provisioning;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.amazonaws.services.kinesis.model.Shard;
|
||||
|
||||
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
|
||||
|
||||
/**
|
||||
* The Kinesis-specific {@link ConsumerDestination} implementation.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
*
|
||||
*/
|
||||
public final class KinesisConsumerDestination implements ConsumerDestination {
|
||||
|
||||
private final String streamName;
|
||||
|
||||
private final List<Shard> shards;
|
||||
|
||||
private final String dlqName;
|
||||
|
||||
public KinesisConsumerDestination(String streamName, List<Shard> shards) {
|
||||
this(streamName, shards, null);
|
||||
}
|
||||
|
||||
public KinesisConsumerDestination(String streamName, List<Shard> shards, String dlqName) {
|
||||
this.streamName = streamName;
|
||||
this.shards = shards;
|
||||
this.dlqName = dlqName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.streamName;
|
||||
}
|
||||
|
||||
public List<Shard> getShards() {
|
||||
return this.shards;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "KinesisConsumerDestination{" + "streamName='" + this.streamName + '\''
|
||||
+ ", shards=" + this.shards + ", dlqName='" + this.dlqName + '\'' + '}';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis.provisioning;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.amazonaws.services.kinesis.model.Shard;
|
||||
|
||||
import org.springframework.cloud.stream.provisioning.ProducerDestination;
|
||||
|
||||
/**
|
||||
* The Kinesis-specific {@link ProducerDestination} implementation.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
*
|
||||
*/
|
||||
public final class KinesisProducerDestination implements ProducerDestination {
|
||||
|
||||
private final String streamName;
|
||||
|
||||
private final List<Shard> shards;
|
||||
|
||||
KinesisProducerDestination(String streamName, List<Shard> shards) {
|
||||
this.streamName = streamName;
|
||||
this.shards = shards;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.streamName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNameForPartition(int shard) {
|
||||
return this.streamName;
|
||||
}
|
||||
|
||||
public List<Shard> getShards() {
|
||||
return this.shards;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "KinesisProducerDestination{" + "streamName='" + this.streamName + '\''
|
||||
+ ", shards=" + this.shards + '}';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Copyright 2017-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis.provisioning;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.amazonaws.services.kinesis.AmazonKinesis;
|
||||
import com.amazonaws.services.kinesis.model.DescribeStreamRequest;
|
||||
import com.amazonaws.services.kinesis.model.DescribeStreamResult;
|
||||
import com.amazonaws.services.kinesis.model.LimitExceededException;
|
||||
import com.amazonaws.services.kinesis.model.ListShardsRequest;
|
||||
import com.amazonaws.services.kinesis.model.ListShardsResult;
|
||||
import com.amazonaws.services.kinesis.model.ResourceNotFoundException;
|
||||
import com.amazonaws.services.kinesis.model.ScalingType;
|
||||
import com.amazonaws.services.kinesis.model.Shard;
|
||||
import com.amazonaws.services.kinesis.model.StreamDescription;
|
||||
import com.amazonaws.services.kinesis.model.StreamStatus;
|
||||
import com.amazonaws.services.kinesis.model.UpdateShardCountRequest;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.stream.binder.ExtendedConsumerProperties;
|
||||
import org.springframework.cloud.stream.binder.ExtendedProducerProperties;
|
||||
import org.springframework.cloud.stream.binder.HeaderMode;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisBinderConfigurationProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisConsumerProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisProducerProperties;
|
||||
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
|
||||
import org.springframework.cloud.stream.provisioning.ProducerDestination;
|
||||
import org.springframework.cloud.stream.provisioning.ProvisioningException;
|
||||
import org.springframework.cloud.stream.provisioning.ProvisioningProvider;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The {@link ProvisioningProvider} implementation for Amazon Kinesis.
|
||||
*
|
||||
* @author Peter Oates
|
||||
* @author Artem Bilan
|
||||
* @author Jacob Severson
|
||||
* @author Sergiu Pantiru
|
||||
* @author Matthias Wesolowski
|
||||
*/
|
||||
public class KinesisStreamProvisioner implements
|
||||
ProvisioningProvider<ExtendedConsumerProperties<KinesisConsumerProperties>,
|
||||
ExtendedProducerProperties<KinesisProducerProperties>> {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(KinesisStreamProvisioner.class);
|
||||
|
||||
private final AmazonKinesis amazonKinesis;
|
||||
|
||||
private final KinesisBinderConfigurationProperties configurationProperties;
|
||||
|
||||
public KinesisStreamProvisioner(AmazonKinesis amazonKinesis,
|
||||
KinesisBinderConfigurationProperties kinesisBinderConfigurationProperties) {
|
||||
|
||||
Assert.notNull(amazonKinesis, "'amazonKinesis' must not be null");
|
||||
Assert.notNull(kinesisBinderConfigurationProperties,
|
||||
"'kinesisBinderConfigurationProperties' must not be null");
|
||||
this.amazonKinesis = amazonKinesis;
|
||||
this.configurationProperties = kinesisBinderConfigurationProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProducerDestination provisionProducerDestination(String name,
|
||||
ExtendedProducerProperties<KinesisProducerProperties> properties)
|
||||
throws ProvisioningException {
|
||||
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Using Kinesis stream for outbound: " + name);
|
||||
}
|
||||
|
||||
if (properties.getHeaderMode() == null) {
|
||||
properties.setHeaderMode(HeaderMode.embeddedHeaders);
|
||||
}
|
||||
|
||||
return new KinesisProducerDestination(name,
|
||||
createOrUpdate(name, properties.getPartitionCount()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConsumerDestination provisionConsumerDestination(String name, String group,
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> properties)
|
||||
throws ProvisioningException {
|
||||
|
||||
if (properties.getExtension().isDynamoDbStreams()) {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Using DynamoDB table in DynamoDB Streams support for inbound: " + name);
|
||||
}
|
||||
return new KinesisConsumerDestination(name, Collections.emptyList());
|
||||
}
|
||||
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Using Kinesis stream for inbound: " + name);
|
||||
}
|
||||
|
||||
if (properties.getHeaderMode() == null) {
|
||||
properties.setHeaderMode(HeaderMode.embeddedHeaders);
|
||||
}
|
||||
|
||||
int shardCount = properties.getInstanceCount() * properties.getConcurrency();
|
||||
|
||||
return new KinesisConsumerDestination(name, createOrUpdate(name, shardCount));
|
||||
}
|
||||
|
||||
private List<Shard> getShardList(String stream) {
|
||||
return this.getShardList(stream, 0);
|
||||
}
|
||||
|
||||
private List<Shard> getShardList(String stream, int retryCount) {
|
||||
List<Shard> shardList = new ArrayList<>();
|
||||
|
||||
if (retryCount++ > configurationProperties.getDescribeStreamRetries()) {
|
||||
ResourceNotFoundException resourceNotFoundException = new ResourceNotFoundException(
|
||||
"The stream [" + stream + "] isn't ACTIVE or doesn't exist.");
|
||||
resourceNotFoundException.setServiceName("Kinesis");
|
||||
|
||||
throw new ProvisioningException(
|
||||
"Kinesis org.springframework.cloud.stream.binder.kinesis.provisioning error",
|
||||
resourceNotFoundException);
|
||||
}
|
||||
|
||||
ListShardsRequest listShardsRequest = new ListShardsRequest().withStreamName(stream);
|
||||
|
||||
try {
|
||||
ListShardsResult listShardsResult = amazonKinesis.listShards(listShardsRequest);
|
||||
|
||||
shardList.addAll(listShardsResult.getShards());
|
||||
|
||||
}
|
||||
catch (LimitExceededException limitExceededException) {
|
||||
logger.info("Got LimitExceededException when describing stream [" + stream + "]. " + "Backing off for ["
|
||||
+ this.configurationProperties.getDescribeStreamBackoff() + "] millis.");
|
||||
|
||||
try {
|
||||
Thread.sleep(this.configurationProperties.getDescribeStreamBackoff());
|
||||
getShardList(stream, retryCount);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ProvisioningException(
|
||||
"The [describeStream] thread for the stream [" + stream + "] has been interrupted.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return shardList;
|
||||
}
|
||||
|
||||
private List<Shard> createOrUpdate(String stream, int shards) {
|
||||
List<Shard> shardList = new ArrayList<>();
|
||||
try {
|
||||
shardList = getShardList(stream);
|
||||
}
|
||||
catch (ResourceNotFoundException ex) {
|
||||
if (!this.configurationProperties.isAutoCreateStream()) {
|
||||
throw new ProvisioningException(
|
||||
"The stream [" + stream + "] was not found and auto creation is disabled.", ex);
|
||||
}
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Stream '" + stream + "' not found. Create one...");
|
||||
}
|
||||
|
||||
this.amazonKinesis.createStream(stream, Math.max(this.configurationProperties.getMinShardCount(), shards));
|
||||
|
||||
waitForStreamToBecomeActive(stream);
|
||||
}
|
||||
|
||||
int effectiveShardCount = Math.max(this.configurationProperties.getMinShardCount(), shards);
|
||||
|
||||
if ((shardList.size() < effectiveShardCount)
|
||||
&& this.configurationProperties.isAutoAddShards()) {
|
||||
return updateShardCount(stream, shardList.size(), effectiveShardCount);
|
||||
}
|
||||
|
||||
return shardList;
|
||||
}
|
||||
|
||||
private void waitForStreamToBecomeActive(String streamName) {
|
||||
int describeStreamRetries = 0;
|
||||
while (true) {
|
||||
try {
|
||||
DescribeStreamResult describeStreamResult = this.amazonKinesis.describeStream(streamName);
|
||||
if (describeStreamResult != null &&
|
||||
StreamStatus.ACTIVE.name().equals(describeStreamResult.getStreamDescription().getStreamStatus())) {
|
||||
|
||||
return;
|
||||
}
|
||||
else if (describeStreamRetries++ > this.configurationProperties.getDescribeStreamRetries()) {
|
||||
ResourceNotFoundException resourceNotFoundException = new ResourceNotFoundException(
|
||||
"The stream [" + streamName + "] isn't ACTIVE or doesn't exist.");
|
||||
resourceNotFoundException.setServiceName("Kinesis");
|
||||
throw new ProvisioningException(
|
||||
"Kinesis org.springframework.cloud.stream.binder.kinesis.provisioning error",
|
||||
resourceNotFoundException);
|
||||
}
|
||||
}
|
||||
catch (LimitExceededException ex) {
|
||||
logger.info("Got LimitExceededException when describing stream [" + streamName + "]. " +
|
||||
"Backing off for [" + this.configurationProperties.getDescribeStreamBackoff() + "] millis.");
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(this.configurationProperties.getDescribeStreamBackoff());
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ProvisioningException(
|
||||
"The [describeStream] thread for the stream [" + streamName + "] has been interrupted.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Shard> updateShardCount(String streamName, int shardCount, int targetCount) {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Stream [" + streamName + "] has [" + shardCount
|
||||
+ "] shards compared to a target configuration of [" + targetCount
|
||||
+ "], creating shards...");
|
||||
}
|
||||
|
||||
UpdateShardCountRequest updateShardCountRequest = new UpdateShardCountRequest()
|
||||
.withStreamName(streamName).withTargetShardCount(targetCount)
|
||||
.withScalingType(ScalingType.UNIFORM_SCALING);
|
||||
|
||||
this.amazonKinesis.updateShardCount(updateShardCountRequest);
|
||||
|
||||
// Wait for stream to become active again after resharding
|
||||
List<Shard> shardList = new ArrayList<>();
|
||||
|
||||
int describeStreamRetries = 0;
|
||||
|
||||
String exclusiveStartShardId = null;
|
||||
|
||||
DescribeStreamRequest describeStreamRequest = new DescribeStreamRequest()
|
||||
.withStreamName(streamName);
|
||||
|
||||
while (true) {
|
||||
DescribeStreamResult describeStreamResult = null;
|
||||
|
||||
try {
|
||||
describeStreamRequest.withExclusiveStartShardId(exclusiveStartShardId);
|
||||
describeStreamResult = this.amazonKinesis.describeStream(describeStreamRequest);
|
||||
StreamDescription streamDescription = describeStreamResult.getStreamDescription();
|
||||
if (StreamStatus.ACTIVE.toString().equals(streamDescription.getStreamStatus())) {
|
||||
shardList.addAll(streamDescription.getShards());
|
||||
|
||||
if (streamDescription.getHasMoreShards()) {
|
||||
exclusiveStartShardId = shardList.get(shardList.size() - 1).getShardId();
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (LimitExceededException ex) {
|
||||
logger.info("Got LimitExceededException when describing stream [" + streamName + "]. "
|
||||
+ "Backing off for [" + this.configurationProperties.getDescribeStreamBackoff() + "] millis.");
|
||||
}
|
||||
|
||||
if (describeStreamResult == null || !StreamStatus.ACTIVE.toString().equals(
|
||||
describeStreamResult.getStreamDescription().getStreamStatus())) {
|
||||
if (describeStreamRetries++ > this.configurationProperties.getDescribeStreamRetries()) {
|
||||
ResourceNotFoundException resourceNotFoundException = new ResourceNotFoundException(
|
||||
"The stream [" + streamName + "] isn't ACTIVE or doesn't exist.");
|
||||
resourceNotFoundException.setServiceName("Kinesis");
|
||||
throw new ProvisioningException(
|
||||
"Kinesis org.springframework.cloud.stream.binder.kinesis.provisioning error",
|
||||
resourceNotFoundException);
|
||||
}
|
||||
try {
|
||||
Thread.sleep(this.configurationProperties.getDescribeStreamBackoff());
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ProvisioningException(
|
||||
"The [describeStream] thread for the stream [" + streamName + "] has been interrupted.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
return shardList;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
kinesis: org.springframework.cloud.stream.binder.kinesis.config.KinesisBinderConfiguration
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2021-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.amazonaws.SDKGlobalConfiguration;
|
||||
import com.amazonaws.services.kinesis.AmazonKinesisAsync;
|
||||
import com.amazonaws.services.kinesis.model.PutRecordsRequest;
|
||||
import com.amazonaws.services.kinesis.model.PutRecordsRequestEntry;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.awspring.cloud.autoconfigure.context.ContextResourceLoaderAutoConfiguration;
|
||||
import io.awspring.cloud.autoconfigure.context.ContextStackAutoConfiguration;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.integration.aws.support.AwsHeaders;
|
||||
import org.springframework.integration.metadata.ConcurrentMetadataStore;
|
||||
import org.springframework.integration.metadata.SimpleMetadataStore;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.integration.support.json.JacksonJsonUtils;
|
||||
import org.springframework.integration.support.locks.DefaultLockRegistry;
|
||||
import org.springframework.integration.support.locks.LockRegistry;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Artem Bilan
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||
properties = {
|
||||
"spring.cloud.stream.bindings.eventConsumerBatchProcessingWithHeaders-in-0.destination=" + KinesisBinderFunctionalTests.KINESIS_STREAM,
|
||||
"spring.cloud.stream.kinesis.bindings.eventConsumerBatchProcessingWithHeaders-in-0.consumer.idleBetweenPolls = 1",
|
||||
"spring.cloud.stream.kinesis.bindings.eventConsumerBatchProcessingWithHeaders-in-0.consumer.listenerMode = batch",
|
||||
"spring.cloud.stream.kinesis.bindings.eventConsumerBatchProcessingWithHeaders-in-0.consumer.checkpointMode = manual",
|
||||
"spring.cloud.stream.kinesis.binder.headers = event.eventType",
|
||||
"spring.cloud.stream.kinesis.binder.autoAddShards = true",
|
||||
"cloud.aws.region.static=eu-west-2" })
|
||||
@DirtiesContext
|
||||
public class KinesisBinderFunctionalTests implements LocalstackContainerTest {
|
||||
|
||||
static final String KINESIS_STREAM = "test_stream";
|
||||
|
||||
private static AmazonKinesisAsync AMAZON_KINESIS;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private CountDownLatch messageBarrier;
|
||||
|
||||
@Autowired
|
||||
private AtomicReference<Message<List<?>>> messageHolder;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
AMAZON_KINESIS = LocalstackContainerTest.kinesisClient();
|
||||
System.setProperty(SDKGlobalConfiguration.AWS_CBOR_DISABLE_SYSTEM_PROPERTY, "true");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testKinesisFunction() throws JsonProcessingException, InterruptedException {
|
||||
PutRecordsRequest putRecordsRequest = new PutRecordsRequest();
|
||||
putRecordsRequest.setStreamName(KINESIS_STREAM);
|
||||
List<PutRecordsRequestEntry> putRecordsRequestEntryList = new ArrayList<>();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
Message<String> eventMessages =
|
||||
MessageBuilder.withPayload("Message" + i)
|
||||
.setHeader("event.eventType", "createEvent")
|
||||
.build();
|
||||
PutRecordsRequestEntry putRecordsRequestEntry = new PutRecordsRequestEntry();
|
||||
byte[] jsonInput = objectMapper.writeValueAsBytes(eventMessages);
|
||||
putRecordsRequestEntry.setData(ByteBuffer.wrap(jsonInput));
|
||||
putRecordsRequestEntry.setPartitionKey("1");
|
||||
putRecordsRequestEntryList.add(putRecordsRequestEntry);
|
||||
}
|
||||
putRecordsRequest.setRecords(putRecordsRequestEntryList);
|
||||
AMAZON_KINESIS.putRecords(putRecordsRequest);
|
||||
|
||||
assertThat(this.messageBarrier.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
Message<List<?>> message = this.messageHolder.get();
|
||||
assertThat(message.getHeaders())
|
||||
.containsKeys(AwsHeaders.CHECKPOINTER,
|
||||
AwsHeaders.SHARD,
|
||||
AwsHeaders.RECEIVED_PARTITION_KEY,
|
||||
AwsHeaders.RECEIVED_STREAM,
|
||||
AwsHeaders.RECEIVED_SEQUENCE_NUMBER)
|
||||
.doesNotContainKeys(AwsHeaders.STREAM, AwsHeaders.PARTITION_KEY);
|
||||
|
||||
List<?> payload = message.getPayload();
|
||||
assertThat(payload).hasSize(10);
|
||||
|
||||
Object item = payload.get(0);
|
||||
|
||||
assertThat(item).isInstanceOf(GenericMessage.class);
|
||||
|
||||
Message<?> messageFromBatch = (Message<?>) item;
|
||||
|
||||
assertThat(messageFromBatch.getPayload()).isEqualTo("Message0");
|
||||
assertThat(messageFromBatch.getHeaders())
|
||||
.containsEntry("event.eventType", "createEvent");
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration(exclude = {
|
||||
ContextResourceLoaderAutoConfiguration.class,
|
||||
ContextStackAutoConfiguration.class })
|
||||
static class TestConfiguration {
|
||||
|
||||
|
||||
@Bean(destroyMethod = "")
|
||||
public AmazonKinesisAsync amazonKinesis() {
|
||||
return AMAZON_KINESIS;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LockRegistry lockRegistry() {
|
||||
return new DefaultLockRegistry();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConcurrentMetadataStore checkpointStore() {
|
||||
return new SimpleMetadataStore();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
return JacksonJsonUtils.messagingAwareMapper();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AtomicReference<Message<List<?>>> messageHolder() {
|
||||
return new AtomicReference<>();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CountDownLatch messageBarrier() {
|
||||
return new CountDownLatch(1);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Consumer<Message<List<?>>> eventConsumerBatchProcessingWithHeaders() {
|
||||
return eventMessages -> {
|
||||
messageHolder().set(eventMessages);
|
||||
messageBarrier().countDown();
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
/*
|
||||
* Copyright 2017-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import com.amazonaws.SDKGlobalConfiguration;
|
||||
import com.amazonaws.handlers.AsyncHandler;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBAsync;
|
||||
import com.amazonaws.services.kinesis.AmazonKinesisAsync;
|
||||
import com.amazonaws.services.kinesis.model.DescribeStreamRequest;
|
||||
import com.amazonaws.services.kinesis.model.DescribeStreamResult;
|
||||
import com.amazonaws.services.kinesis.model.PutRecordRequest;
|
||||
import com.amazonaws.services.kinesis.model.PutRecordResult;
|
||||
import com.amazonaws.services.kinesis.model.Record;
|
||||
import com.amazonaws.services.kinesis.model.Shard;
|
||||
import com.amazonaws.services.kinesis.model.ShardIteratorType;
|
||||
import com.amazonaws.services.kinesis.model.StreamDescription;
|
||||
import com.amazonaws.services.kinesis.model.StreamStatus;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.mockito.BDDMockito;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.cloud.stream.binder.Binding;
|
||||
import org.springframework.cloud.stream.binder.ExtendedConsumerProperties;
|
||||
import org.springframework.cloud.stream.binder.ExtendedProducerProperties;
|
||||
import org.springframework.cloud.stream.binder.PartitionCapableBinderTests;
|
||||
import org.springframework.cloud.stream.binder.Spy;
|
||||
import org.springframework.cloud.stream.binder.TestUtils;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisBinderConfigurationProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisConsumerProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisProducerProperties;
|
||||
import org.springframework.cloud.stream.config.BindingProperties;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.expression.common.LiteralExpression;
|
||||
import org.springframework.integration.aws.inbound.kinesis.KinesisShardOffset;
|
||||
import org.springframework.integration.aws.inbound.kinesis.ListenerMode;
|
||||
import org.springframework.integration.aws.support.AwsRequestFailureException;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.SubscribableChannel;
|
||||
import org.springframework.messaging.support.ErrorMessage;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* The tests for Kinesis Binder.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
* @author Jacob Severson
|
||||
* @author Arnaud Lecollaire
|
||||
*/
|
||||
public class KinesisBinderTests extends
|
||||
PartitionCapableBinderTests<KinesisTestBinder, ExtendedConsumerProperties<KinesisConsumerProperties>,
|
||||
ExtendedProducerProperties<KinesisProducerProperties>>
|
||||
implements LocalstackContainerTest {
|
||||
|
||||
private static final String CLASS_UNDER_TEST_NAME = KinesisBinderTests.class
|
||||
.getSimpleName();
|
||||
|
||||
private static AmazonKinesisAsync AMAZON_KINESIS;
|
||||
|
||||
private static AmazonDynamoDBAsync DYNAMO_DB;
|
||||
|
||||
|
||||
public KinesisBinderTests() {
|
||||
this.timeoutMultiplier = 10D;
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() {
|
||||
AMAZON_KINESIS = LocalstackContainerTest.kinesisClient();
|
||||
DYNAMO_DB = LocalstackContainerTest.dynamoDbClient();
|
||||
System.setProperty(SDKGlobalConfiguration.AWS_CBOR_DISABLE_SYSTEM_PROPERTY, "true");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
public void testClean(TestInfo testInfo) {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
public void testPartitionedModuleSpEL(TestInfo testInfo) {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutoCreateStreamForNonExistingStream() throws Exception {
|
||||
KinesisTestBinder binder = getBinder();
|
||||
DirectChannel output = createBindableChannel("output", new BindingProperties());
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> consumerProperties = createConsumerProperties();
|
||||
Date testDate = new Date();
|
||||
consumerProperties.getExtension().setShardIteratorType(
|
||||
ShardIteratorType.AT_TIMESTAMP.name() + ":" + testDate.getTime());
|
||||
String testStreamName = "nonexisting" + System.currentTimeMillis();
|
||||
Binding<?> binding = binder.bindConsumer(testStreamName, "test", output,
|
||||
consumerProperties);
|
||||
binding.unbind();
|
||||
|
||||
DescribeStreamResult streamResult = AMAZON_KINESIS.describeStream(testStreamName);
|
||||
String createdStreamName = streamResult.getStreamDescription().getStreamName();
|
||||
int createdShards = streamResult.getStreamDescription().getShards().size();
|
||||
String createdStreamStatus = streamResult.getStreamDescription()
|
||||
.getStreamStatus();
|
||||
|
||||
assertThat(createdStreamName).isEqualTo(testStreamName);
|
||||
assertThat(createdShards).isEqualTo(consumerProperties.getInstanceCount()
|
||||
* consumerProperties.getConcurrency());
|
||||
assertThat(createdStreamStatus).isEqualTo(StreamStatus.ACTIVE.toString());
|
||||
|
||||
KinesisShardOffset shardOffset = TestUtils.getPropertyValue(binding,
|
||||
"lifecycle.streamInitialSequence", KinesisShardOffset.class);
|
||||
assertThat(shardOffset.getIteratorType())
|
||||
.isEqualTo(ShardIteratorType.AT_TIMESTAMP);
|
||||
assertThat(shardOffset.getTimestamp()).isEqualTo(testDate);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testAnonymousGroup(TestInfo testInfo) throws Exception {
|
||||
KinesisTestBinder binder = getBinder();
|
||||
ExtendedProducerProperties<KinesisProducerProperties> producerProperties = createProducerProperties();
|
||||
DirectChannel output = createBindableChannel("output",
|
||||
createProducerBindingProperties(producerProperties));
|
||||
|
||||
Binding<MessageChannel> producerBinding = binder.bindProducer(
|
||||
String.format("defaultGroup%s0", getDestinationNameDelimiter()), output,
|
||||
producerProperties);
|
||||
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> consumerProperties = createConsumerProperties();
|
||||
consumerProperties.setConcurrency(2);
|
||||
consumerProperties.setInstanceCount(3);
|
||||
consumerProperties.setInstanceIndex(0);
|
||||
|
||||
QueueChannel input1 = new QueueChannel();
|
||||
Binding<MessageChannel> binding1 = binder.bindConsumer(
|
||||
String.format("defaultGroup%s0", getDestinationNameDelimiter()), null,
|
||||
input1, consumerProperties);
|
||||
|
||||
consumerProperties.setInstanceIndex(1);
|
||||
|
||||
QueueChannel input2 = new QueueChannel();
|
||||
Binding<MessageChannel> binding2 = binder.bindConsumer(
|
||||
String.format("defaultGroup%s0", getDestinationNameDelimiter()), null,
|
||||
input2, consumerProperties);
|
||||
|
||||
String testPayload1 = "foo-" + UUID.randomUUID().toString();
|
||||
output.send(MessageBuilder.withPayload(testPayload1)
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN)
|
||||
.build());
|
||||
|
||||
Message<byte[]> receivedMessage1 = (Message<byte[]>) receive(input1);
|
||||
assertThat(receivedMessage1).isNotNull();
|
||||
assertThat(new String(receivedMessage1.getPayload())).isEqualTo(testPayload1);
|
||||
|
||||
Message<byte[]> receivedMessage2 = (Message<byte[]>) receive(input2);
|
||||
assertThat(receivedMessage2).isNotNull();
|
||||
assertThat(new String(receivedMessage2.getPayload())).isEqualTo(testPayload1);
|
||||
|
||||
binding2.unbind();
|
||||
|
||||
String testPayload2 = "foo-" + UUID.randomUUID().toString();
|
||||
output.send(MessageBuilder.withPayload(testPayload2)
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN)
|
||||
.build());
|
||||
|
||||
binding2 = binder.bindConsumer(
|
||||
String.format("defaultGroup%s0", getDestinationNameDelimiter()), null,
|
||||
input2, consumerProperties);
|
||||
String testPayload3 = "foo-" + UUID.randomUUID().toString();
|
||||
output.send(MessageBuilder.withPayload(testPayload3)
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN)
|
||||
.build());
|
||||
|
||||
receivedMessage1 = (Message<byte[]>) receive(input1);
|
||||
assertThat(receivedMessage1).isNotNull();
|
||||
assertThat(new String(receivedMessage1.getPayload())).isEqualTo(testPayload2);
|
||||
receivedMessage1 = (Message<byte[]>) receive(input1);
|
||||
assertThat(receivedMessage1).isNotNull();
|
||||
assertThat(new String(receivedMessage1.getPayload())).isNotNull();
|
||||
|
||||
receivedMessage2 = (Message<byte[]>) receive(input2);
|
||||
assertThat(receivedMessage2).isNotNull();
|
||||
assertThat(new String(receivedMessage2.getPayload())).isEqualTo(testPayload1);
|
||||
|
||||
receivedMessage2 = (Message<byte[]>) receive(input2);
|
||||
assertThat(receivedMessage2).isNotNull();
|
||||
assertThat(new String(receivedMessage2.getPayload())).isEqualTo(testPayload2);
|
||||
|
||||
receivedMessage2 = (Message<byte[]>) receive(input2);
|
||||
assertThat(receivedMessage2).isNotNull();
|
||||
assertThat(new String(receivedMessage2.getPayload())).isEqualTo(testPayload3);
|
||||
|
||||
producerBinding.unbind();
|
||||
binding1.unbind();
|
||||
binding2.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testProducerErrorChannel() throws Exception {
|
||||
KinesisTestBinder binder = getBinder();
|
||||
|
||||
final RuntimeException putRecordException = new RuntimeException(
|
||||
"putRecordRequestEx");
|
||||
final AtomicReference<Object> sent = new AtomicReference<>();
|
||||
AmazonKinesisAsync amazonKinesisMock = mock(AmazonKinesisAsync.class);
|
||||
BDDMockito
|
||||
.given(amazonKinesisMock.putRecordAsync(any(PutRecordRequest.class),
|
||||
any(AsyncHandler.class)))
|
||||
.willAnswer((Answer<Future<PutRecordResult>>) (invocation) -> {
|
||||
PutRecordRequest request = invocation.getArgument(0);
|
||||
sent.set(request.getData());
|
||||
AsyncHandler<?, ?> handler = invocation.getArgument(1);
|
||||
handler.onError(putRecordException);
|
||||
return mock(Future.class);
|
||||
});
|
||||
|
||||
new DirectFieldAccessor(binder.getBinder()).setPropertyValue("amazonKinesis",
|
||||
amazonKinesisMock);
|
||||
|
||||
ExtendedProducerProperties<KinesisProducerProperties> producerProps = createProducerProperties();
|
||||
producerProps.setErrorChannelEnabled(true);
|
||||
DirectChannel moduleOutputChannel = createBindableChannel("output",
|
||||
createProducerBindingProperties(producerProps));
|
||||
Binding<MessageChannel> producerBinding = binder.bindProducer("ec.0",
|
||||
moduleOutputChannel, producerProps);
|
||||
|
||||
ApplicationContext applicationContext = TestUtils.getPropertyValue(
|
||||
binder.getBinder(), "applicationContext", ApplicationContext.class);
|
||||
SubscribableChannel ec = applicationContext.getBean("ec.0.errors",
|
||||
SubscribableChannel.class);
|
||||
final AtomicReference<Message<?>> errorMessage = new AtomicReference<>();
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
ec.subscribe((message) -> {
|
||||
errorMessage.set(message);
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
String messagePayload = "oops";
|
||||
moduleOutputChannel.send(new GenericMessage<>(messagePayload.getBytes()));
|
||||
|
||||
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(errorMessage.get()).isInstanceOf(ErrorMessage.class);
|
||||
assertThat(errorMessage.get().getPayload())
|
||||
.isInstanceOf(AwsRequestFailureException.class);
|
||||
AwsRequestFailureException exception = (AwsRequestFailureException) errorMessage
|
||||
.get().getPayload();
|
||||
assertThat(exception.getCause()).isSameAs(putRecordException);
|
||||
assertThat(((PutRecordRequest) exception.getRequest()).getData())
|
||||
.isSameAs(sent.get());
|
||||
producerBinding.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testBatchListener() throws Exception {
|
||||
KinesisTestBinder binder = getBinder();
|
||||
ExtendedProducerProperties<KinesisProducerProperties> producerProperties = createProducerProperties();
|
||||
DirectChannel output = createBindableChannel("output",
|
||||
createProducerBindingProperties(producerProperties));
|
||||
|
||||
Binding<MessageChannel> outputBinding = binder.bindProducer("testBatchListener",
|
||||
output, producerProperties);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
output.send(new GenericMessage<>(i));
|
||||
}
|
||||
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> consumerProperties = createConsumerProperties();
|
||||
consumerProperties.getExtension().setListenerMode(ListenerMode.batch);
|
||||
consumerProperties.setUseNativeDecoding(true);
|
||||
|
||||
QueueChannel input = new QueueChannel();
|
||||
Binding<MessageChannel> inputBinding = binder.bindConsumer("testBatchListener",
|
||||
null, input, consumerProperties);
|
||||
|
||||
Message<List<?>> receivedMessage = (Message<List<?>>) receive(input);
|
||||
assertThat(receivedMessage).isNotNull();
|
||||
assertThat(receivedMessage.getPayload().size()).isEqualTo(3);
|
||||
|
||||
receivedMessage.getPayload().forEach((r) -> {
|
||||
assertThat(r).isInstanceOf(Record.class);
|
||||
});
|
||||
|
||||
outputBinding.unbind();
|
||||
inputBinding.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("Localstack doesn't support updateShardCount. Test only against real AWS Kinesis")
|
||||
public void testPartitionCountIncreasedIfAutoAddPartitionsSet() throws Exception {
|
||||
KinesisBinderConfigurationProperties configurationProperties = new KinesisBinderConfigurationProperties();
|
||||
|
||||
String stream = "existing" + System.currentTimeMillis();
|
||||
|
||||
AMAZON_KINESIS.createStream(stream, 1);
|
||||
|
||||
List<Shard> shards = describeStream(stream);
|
||||
|
||||
assertThat(shards.size()).isEqualTo(1);
|
||||
|
||||
configurationProperties.setMinShardCount(6);
|
||||
configurationProperties.setAutoAddShards(true);
|
||||
KinesisTestBinder binder = getBinder(configurationProperties);
|
||||
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> consumerProperties = createConsumerProperties();
|
||||
DirectChannel output = createBindableChannel("output", new BindingProperties());
|
||||
Binding<?> binding = binder.bindConsumer(stream, "test", output, consumerProperties);
|
||||
binding.unbind();
|
||||
|
||||
shards = describeStream(stream);
|
||||
|
||||
assertThat(shards.size()).isEqualTo(6);
|
||||
}
|
||||
|
||||
private List<Shard> describeStream(String stream) {
|
||||
String exclusiveStartShardId = null;
|
||||
|
||||
DescribeStreamRequest describeStreamRequest = new DescribeStreamRequest()
|
||||
.withStreamName(stream);
|
||||
|
||||
List<Shard> shardList = new ArrayList<>();
|
||||
|
||||
while (true) {
|
||||
DescribeStreamResult describeStreamResult;
|
||||
|
||||
describeStreamRequest.withExclusiveStartShardId(exclusiveStartShardId);
|
||||
describeStreamResult = AMAZON_KINESIS.describeStream(describeStreamRequest);
|
||||
StreamDescription streamDescription = describeStreamResult
|
||||
.getStreamDescription();
|
||||
if (StreamStatus.ACTIVE.toString()
|
||||
.equals(streamDescription.getStreamStatus())) {
|
||||
shardList.addAll(streamDescription.getShards());
|
||||
|
||||
if (streamDescription.getHasMoreShards()) {
|
||||
exclusiveStartShardId = shardList.get(shardList.size() - 1)
|
||||
.getShardId();
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
return shardList;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean usesExplicitRouting() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getClassUnderTestName() {
|
||||
return CLASS_UNDER_TEST_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected KinesisTestBinder getBinder() {
|
||||
return getBinder(new KinesisBinderConfigurationProperties());
|
||||
}
|
||||
|
||||
private KinesisTestBinder getBinder(
|
||||
KinesisBinderConfigurationProperties kinesisBinderConfigurationProperties) {
|
||||
if (this.testBinder == null) {
|
||||
this.testBinder = new KinesisTestBinder(AMAZON_KINESIS, DYNAMO_DB, kinesisBinderConfigurationProperties);
|
||||
this.timeoutMultiplier = 20;
|
||||
}
|
||||
return this.testBinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ExtendedConsumerProperties<KinesisConsumerProperties> createConsumerProperties() {
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> kinesisConsumerProperties = new ExtendedConsumerProperties<>(
|
||||
new KinesisConsumerProperties());
|
||||
// set the default values that would normally be propagated by Spring Cloud Stream
|
||||
kinesisConsumerProperties.setInstanceCount(1);
|
||||
kinesisConsumerProperties.setInstanceIndex(0);
|
||||
kinesisConsumerProperties.getExtension().setShardIteratorType(ShardIteratorType.TRIM_HORIZON.name());
|
||||
return kinesisConsumerProperties;
|
||||
}
|
||||
|
||||
private ExtendedProducerProperties<KinesisProducerProperties> createProducerProperties() {
|
||||
return this.createProducerProperties(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ExtendedProducerProperties<KinesisProducerProperties> createProducerProperties(TestInfo testInto) {
|
||||
ExtendedProducerProperties<KinesisProducerProperties> producerProperties = new ExtendedProducerProperties<>(
|
||||
new KinesisProducerProperties());
|
||||
producerProperties.setPartitionKeyExpression(new LiteralExpression("1"));
|
||||
producerProperties.getExtension().setSync(true);
|
||||
return producerProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spy spyOn(String name) {
|
||||
throw new UnsupportedOperationException("'spyOn' is not used by Kinesis tests");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2017-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.amazonaws.auth.AWSStaticCredentialsProvider;
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBAsync;
|
||||
import com.amazonaws.services.kinesis.AmazonKinesisAsync;
|
||||
import com.amazonaws.services.kinesis.model.ListStreamsRequest;
|
||||
import com.amazonaws.services.kinesis.model.ListStreamsResult;
|
||||
import com.amazonaws.services.kinesis.model.ResourceNotFoundException;
|
||||
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.cloud.stream.binder.AbstractTestBinder;
|
||||
import org.springframework.cloud.stream.binder.ExtendedConsumerProperties;
|
||||
import org.springframework.cloud.stream.binder.ExtendedProducerProperties;
|
||||
import org.springframework.cloud.stream.binder.PartitionTestSupport;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisBinderConfigurationProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisConsumerProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisProducerProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.provisioning.KinesisStreamProvisioner;
|
||||
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.integration.config.EnableIntegration;
|
||||
import org.springframework.integration.core.MessageProducer;
|
||||
|
||||
/**
|
||||
* An {@link AbstractTestBinder} implementation for the {@link KinesisMessageChannelBinder}.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
* @author Arnaud Lecollaire
|
||||
*/
|
||||
public class KinesisTestBinder extends
|
||||
AbstractTestBinder<KinesisMessageChannelBinder, ExtendedConsumerProperties<KinesisConsumerProperties>,
|
||||
ExtendedProducerProperties<KinesisProducerProperties>> {
|
||||
|
||||
private final AmazonKinesisAsync amazonKinesis;
|
||||
|
||||
private final GenericApplicationContext applicationContext;
|
||||
|
||||
public KinesisTestBinder(AmazonKinesisAsync amazonKinesis, AmazonDynamoDBAsync dynamoDbClient,
|
||||
KinesisBinderConfigurationProperties kinesisBinderConfigurationProperties) {
|
||||
|
||||
this.applicationContext = new AnnotationConfigApplicationContext(Config.class);
|
||||
|
||||
this.amazonKinesis = amazonKinesis;
|
||||
|
||||
KinesisStreamProvisioner provisioningProvider = new KinesisStreamProvisioner(
|
||||
amazonKinesis, kinesisBinderConfigurationProperties);
|
||||
|
||||
KinesisMessageChannelBinder binder = new TestKinesisMessageChannelBinder(
|
||||
amazonKinesis, dynamoDbClient, kinesisBinderConfigurationProperties,
|
||||
provisioningProvider);
|
||||
|
||||
binder.setApplicationContext(this.applicationContext);
|
||||
|
||||
setBinder(binder);
|
||||
}
|
||||
|
||||
public GenericApplicationContext getApplicationContext() {
|
||||
return this.applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
ListStreamsRequest listStreamsRequest = new ListStreamsRequest();
|
||||
ListStreamsResult listStreamsResult = this.amazonKinesis
|
||||
.listStreams(listStreamsRequest);
|
||||
|
||||
List<String> streamNames = listStreamsResult.getStreamNames();
|
||||
|
||||
while (listStreamsResult.getHasMoreStreams()) {
|
||||
if (streamNames.size() > 0) {
|
||||
listStreamsRequest.setExclusiveStartStreamName(
|
||||
streamNames.get(streamNames.size() - 1));
|
||||
}
|
||||
listStreamsResult = this.amazonKinesis.listStreams(listStreamsRequest);
|
||||
streamNames.addAll(listStreamsResult.getStreamNames());
|
||||
}
|
||||
|
||||
for (String stream : streamNames) {
|
||||
this.amazonKinesis.deleteStream(stream);
|
||||
while (true) {
|
||||
try {
|
||||
this.amazonKinesis.describeStream(stream);
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
catch (ResourceNotFoundException ex) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test configuration.
|
||||
*/
|
||||
@Configuration
|
||||
@EnableIntegration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public PartitionTestSupport partitionSupport() {
|
||||
return new PartitionTestSupport();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestKinesisMessageChannelBinder
|
||||
extends KinesisMessageChannelBinder {
|
||||
|
||||
TestKinesisMessageChannelBinder(AmazonKinesisAsync amazonKinesis,
|
||||
AmazonDynamoDBAsync dynamoDbClient,
|
||||
KinesisBinderConfigurationProperties kinesisBinderConfigurationProperties,
|
||||
KinesisStreamProvisioner provisioningProvider) {
|
||||
|
||||
super(kinesisBinderConfigurationProperties, provisioningProvider, amazonKinesis,
|
||||
new AWSStaticCredentialsProvider(new BasicAWSCredentials("", "")), dynamoDbClient, null, null);
|
||||
}
|
||||
|
||||
/*
|
||||
* Some tests use multiple instance indexes for the same topic; we need to make
|
||||
* the error infrastructure beans unique.
|
||||
*/
|
||||
@Override
|
||||
protected String errorsBaseName(ConsumerDestination destination, String group,
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> consumerProperties) {
|
||||
return super.errorsBaseName(destination, group, consumerProperties) + "-"
|
||||
+ consumerProperties.getInstanceIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageProducer createConsumerEndpoint(ConsumerDestination destination,
|
||||
String group,
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> properties) {
|
||||
|
||||
MessageProducer messageProducer = super.createConsumerEndpoint(destination,
|
||||
group, properties);
|
||||
DirectFieldAccessor dfa = new DirectFieldAccessor(messageProducer);
|
||||
dfa.setPropertyValue("describeStreamBackoff", 10);
|
||||
dfa.setPropertyValue("consumerBackoff", 10);
|
||||
dfa.setPropertyValue("idleBetweenPolls", 1);
|
||||
return messageProducer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2022-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis;
|
||||
|
||||
import com.amazonaws.client.builder.AwsClientBuilder;
|
||||
import com.amazonaws.services.cloudwatch.AmazonCloudWatch;
|
||||
import com.amazonaws.services.cloudwatch.AmazonCloudWatchClientBuilder;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBAsync;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBAsyncClientBuilder;
|
||||
import com.amazonaws.services.kinesis.AmazonKinesisAsync;
|
||||
import com.amazonaws.services.kinesis.AmazonKinesisAsyncClientBuilder;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
import org.testcontainers.containers.localstack.LocalStackContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
/**
|
||||
*
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 4.0
|
||||
*/
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
@DisabledOnOs(OS.MAC)
|
||||
public interface LocalstackContainerTest {
|
||||
|
||||
@Container
|
||||
LocalStackContainer localStack =
|
||||
new LocalStackContainer(DockerImageName.parse(TestUtils.dockerRegistryFromEnv() + "localstack/localstack"))
|
||||
.withServices(
|
||||
LocalStackContainer.Service.DYNAMODB,
|
||||
LocalStackContainer.Service.KINESIS,
|
||||
LocalStackContainer.Service.CLOUDWATCH);
|
||||
|
||||
|
||||
static AmazonDynamoDBAsync dynamoDbClient() {
|
||||
return applyAwsClientOptions(AmazonDynamoDBAsyncClientBuilder.standard(), LocalStackContainer.Service.DYNAMODB);
|
||||
}
|
||||
|
||||
static AmazonKinesisAsync kinesisClient() {
|
||||
return applyAwsClientOptions(AmazonKinesisAsyncClientBuilder.standard(), LocalStackContainer.Service.KINESIS);
|
||||
}
|
||||
|
||||
static AmazonCloudWatch cloudWatchClient() {
|
||||
return applyAwsClientOptions(AmazonCloudWatchClientBuilder.standard(), LocalStackContainer.Service.CLOUDWATCH);
|
||||
}
|
||||
|
||||
private static <B extends AwsClientBuilder<B, T>, T> T applyAwsClientOptions(B clientBuilder,
|
||||
LocalStackContainer.Service serviceToBuild) {
|
||||
|
||||
return clientBuilder.withEndpointConfiguration(localStack.getEndpointConfiguration(serviceToBuild))
|
||||
.withCredentials(localStack.getDefaultCredentialsProvider())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis.adapter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import com.amazonaws.regions.Regions;
|
||||
import com.amazonaws.services.dynamodbv2.AbstractAmazonDynamoDBStreams;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBStreams;
|
||||
import com.amazonaws.services.dynamodbv2.model.AmazonDynamoDBException;
|
||||
import com.amazonaws.services.dynamodbv2.model.DescribeStreamRequest;
|
||||
import com.amazonaws.services.dynamodbv2.model.DescribeStreamResult;
|
||||
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
|
||||
import com.amazonaws.services.dynamodbv2.model.KeyType;
|
||||
import com.amazonaws.services.dynamodbv2.model.SequenceNumberRange;
|
||||
import com.amazonaws.services.dynamodbv2.model.Shard;
|
||||
import com.amazonaws.services.dynamodbv2.model.StreamDescription;
|
||||
import com.amazonaws.services.dynamodbv2.model.StreamStatus;
|
||||
import com.amazonaws.services.dynamodbv2.model.StreamViewType;
|
||||
import com.amazonaws.services.kinesis.model.InvalidArgumentException;
|
||||
import com.amazonaws.services.kinesis.model.ListShardsRequest;
|
||||
import com.amazonaws.services.kinesis.model.ListShardsResult;
|
||||
import com.amazonaws.services.kinesis.model.ResourceNotFoundException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* @author Asiel Caballero
|
||||
*
|
||||
* @since 2.0.3
|
||||
*/
|
||||
public class SpringDynamoDBAdapterClientTests {
|
||||
|
||||
private final AmazonDynamoDBStreams amazonDynamoDBStreams = new InMemoryAmazonDynamoDBStreams();
|
||||
|
||||
private final SpringDynamoDBAdapterClient springDynamoDBAdapterClient =
|
||||
new SpringDynamoDBAdapterClient(amazonDynamoDBStreams);
|
||||
|
||||
@Test
|
||||
public void listShardsForNotFoundStream() {
|
||||
assertThatExceptionOfType(ResourceNotFoundException.class)
|
||||
.isThrownBy(() -> springDynamoDBAdapterClient.listShards(new ListShardsRequest()
|
||||
.withStreamName("not-found")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listShardsWithInvalidToken() {
|
||||
assertThatExceptionOfType(InvalidArgumentException.class)
|
||||
.isThrownBy(() -> springDynamoDBAdapterClient.listShards(new ListShardsRequest()
|
||||
.withNextToken("invalid-token")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listShardsWithTokenAndStreamName() {
|
||||
assertThatExceptionOfType(InvalidArgumentException.class)
|
||||
.isThrownBy(() ->
|
||||
springDynamoDBAdapterClient.listShards(new ListShardsRequest()
|
||||
.withStreamName(InMemoryAmazonDynamoDBStreams.STREAM_ARN)
|
||||
.withNextToken("valid!!##%%token")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listShardsNoPagination() {
|
||||
ListShardsResult listShards = springDynamoDBAdapterClient.listShards(new ListShardsRequest()
|
||||
.withStreamName(InMemoryAmazonDynamoDBStreams.STREAM_ARN));
|
||||
|
||||
assertThat(listShards.getNextToken()).isNull();
|
||||
assertThat(listShards.getShards().size()).isEqualTo(InMemoryAmazonDynamoDBStreams.SHARDS.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listShardWithTokenPagination() {
|
||||
int maxResults = (InMemoryAmazonDynamoDBStreams.SHARDS.size() / 2)
|
||||
+ (InMemoryAmazonDynamoDBStreams.SHARDS.size() % 2);
|
||||
ListShardsResult listShards = springDynamoDBAdapterClient.listShards(new ListShardsRequest()
|
||||
.withStreamName(InMemoryAmazonDynamoDBStreams.STREAM_ARN)
|
||||
.withMaxResults(maxResults));
|
||||
|
||||
assertThat(listShards.getNextToken()).isNotNull();
|
||||
assertThat(listShards.getShards().size()).isEqualTo(maxResults);
|
||||
|
||||
listShards = springDynamoDBAdapterClient.listShards(new ListShardsRequest()
|
||||
.withNextToken(listShards.getNextToken()));
|
||||
|
||||
assertThat(listShards.getNextToken()).isNull();
|
||||
assertThat(listShards.getShards().size()).isEqualTo(InMemoryAmazonDynamoDBStreams.SHARDS.size() - maxResults);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listShardsWithShardIdPagination() {
|
||||
ListShardsResult listShards = springDynamoDBAdapterClient.listShards(new ListShardsRequest()
|
||||
.withStreamName(InMemoryAmazonDynamoDBStreams.STREAM_ARN)
|
||||
.withExclusiveStartShardId(InMemoryAmazonDynamoDBStreams.SHARDS.get(2).getShardId())
|
||||
.withMaxResults(1));
|
||||
|
||||
assertThat(listShards.getNextToken()).isNotNull();
|
||||
assertThat(listShards.getShards().size()).isEqualTo(1);
|
||||
assertThat(listShards.getShards().get(0).getShardId())
|
||||
.isEqualTo(InMemoryAmazonDynamoDBStreams.SHARDS.get(3).getShardId());
|
||||
}
|
||||
|
||||
|
||||
private static class InMemoryAmazonDynamoDBStreams extends AbstractAmazonDynamoDBStreams {
|
||||
|
||||
private static final String TABLE_NAME = "test-streams";
|
||||
|
||||
private static final String STREAM_LABEL = "2020-10-21T11:49:13.355";
|
||||
|
||||
private static final String STREAM_ARN = String
|
||||
.format("arn:aws:dynamodb:%s:%s:table/%s/stream/%s",
|
||||
Regions.DEFAULT_REGION.getName(), "000000000000", TABLE_NAME, STREAM_LABEL);
|
||||
|
||||
private static final List<Shard> SHARDS =
|
||||
Arrays.asList(
|
||||
buildShard("shardId-00000001603195033866-c5d0c2b1", "51100000000002515059163", "51300000000002521847055"),
|
||||
buildShard("shardId-00000001603208699318-b15c42af", "804300000000026046960744", "804300000000026046960744")
|
||||
.withParentShardId("shardId-00000001603195033866-c5d0c2b1"),
|
||||
buildShard("shardId-00000001603223404428-90b80e6c", "1613900000000033324335703", "1613900000000033324335703")
|
||||
.withParentShardId("shardId-00000001603208699318-b15c42af"),
|
||||
buildShard("shardId-00000001603237029376-bd9c40dd", "2364600000000001701561758", "2364600000000001701561758")
|
||||
.withParentShardId("shardId-00000001603223404428-90b80e6c"),
|
||||
buildShard("shardId-00000001603249855034-b917a47f", "3071400000000035046301998", "3071400000000035046301998")
|
||||
.withParentShardId("shardId-00000001603237029376-bd9c40dd"));
|
||||
|
||||
private final StreamDescription streamDescription = new StreamDescription()
|
||||
.withStreamArn(STREAM_ARN)
|
||||
.withStreamLabel(STREAM_LABEL)
|
||||
.withStreamViewType(StreamViewType.KEYS_ONLY)
|
||||
.withCreationRequestDateTime(new Date())
|
||||
.withTableName(TABLE_NAME)
|
||||
.withKeySchema(new KeySchemaElement("name", KeyType.HASH))
|
||||
.withStreamStatus(StreamStatus.ENABLED);
|
||||
|
||||
private static Shard buildShard(String parentShardIdString, String startingSequenceNumber,
|
||||
String endingSequenceNumber) {
|
||||
|
||||
return new Shard()
|
||||
.withShardId(parentShardIdString)
|
||||
.withSequenceNumberRange(new SequenceNumberRange()
|
||||
.withStartingSequenceNumber(startingSequenceNumber)
|
||||
.withEndingSequenceNumber(endingSequenceNumber));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DescribeStreamResult describeStream(DescribeStreamRequest request) {
|
||||
// Invalid StreamArn (Service: AmazonDynamoDBStreams; Status Code: 400;
|
||||
// Error Code: ValidationException; Request ID: <Request ID>; Proxy: null)
|
||||
if (!STREAM_ARN.equals(request.getStreamArn())) {
|
||||
throw new AmazonDynamoDBException("Invalid StreamArn");
|
||||
}
|
||||
|
||||
int limit = request.getLimit() != null ? request.getLimit() : 100;
|
||||
|
||||
int shardIndex = request.getExclusiveStartShardId() != null
|
||||
? findShardIndex(request.getExclusiveStartShardId())
|
||||
: 0;
|
||||
int lastShardIndex = Math.min(shardIndex + limit, SHARDS.size());
|
||||
streamDescription.setShards(SHARDS.subList(shardIndex, lastShardIndex));
|
||||
streamDescription.setLastEvaluatedShardId(
|
||||
lastShardIndex != SHARDS.size() ? SHARDS.get(lastShardIndex).getShardId() : null);
|
||||
|
||||
return new DescribeStreamResult()
|
||||
.withStreamDescription(streamDescription);
|
||||
}
|
||||
|
||||
private int findShardIndex(String shardId) {
|
||||
int i = 0;
|
||||
// checkstyle forced this way of writing it
|
||||
while (i < SHARDS.size() && !SHARDS.get(i).getShardId().equals(shardId)) {
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i + 1 >= SHARDS.size()) {
|
||||
throw new RuntimeException("ShardId not found");
|
||||
}
|
||||
|
||||
return i + 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
* Copyright 2017-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.binder.kinesis.provisioning;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.amazonaws.services.kinesis.AmazonKinesis;
|
||||
import com.amazonaws.services.kinesis.model.CreateStreamResult;
|
||||
import com.amazonaws.services.kinesis.model.DescribeStreamRequest;
|
||||
import com.amazonaws.services.kinesis.model.DescribeStreamResult;
|
||||
import com.amazonaws.services.kinesis.model.ListShardsRequest;
|
||||
import com.amazonaws.services.kinesis.model.ListShardsResult;
|
||||
import com.amazonaws.services.kinesis.model.ResourceNotFoundException;
|
||||
import com.amazonaws.services.kinesis.model.ScalingType;
|
||||
import com.amazonaws.services.kinesis.model.Shard;
|
||||
import com.amazonaws.services.kinesis.model.StreamDescription;
|
||||
import com.amazonaws.services.kinesis.model.StreamStatus;
|
||||
import com.amazonaws.services.kinesis.model.UpdateShardCountRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.cloud.stream.binder.ExtendedConsumerProperties;
|
||||
import org.springframework.cloud.stream.binder.ExtendedProducerProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisBinderConfigurationProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisConsumerProperties;
|
||||
import org.springframework.cloud.stream.binder.kinesis.properties.KinesisProducerProperties;
|
||||
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
|
||||
import org.springframework.cloud.stream.provisioning.ProducerDestination;
|
||||
import org.springframework.cloud.stream.provisioning.ProvisioningException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for the {@link KinesisStreamProvisioner}.
|
||||
*
|
||||
* @author Jacob Severson
|
||||
* @author Artem Bilan
|
||||
* @author Sergiu Pantiru
|
||||
*/
|
||||
class KinesisStreamProvisionerTests {
|
||||
|
||||
@Test
|
||||
void testProvisionProducerSuccessfulWithExistingStream() {
|
||||
AmazonKinesis amazonKinesisMock = mock(AmazonKinesis.class);
|
||||
KinesisBinderConfigurationProperties binderProperties = new KinesisBinderConfigurationProperties();
|
||||
KinesisStreamProvisioner provisioner = new KinesisStreamProvisioner(
|
||||
amazonKinesisMock, binderProperties);
|
||||
ExtendedProducerProperties<KinesisProducerProperties> extendedProducerProperties =
|
||||
new ExtendedProducerProperties<>(new KinesisProducerProperties());
|
||||
String name = "test-stream";
|
||||
|
||||
when(amazonKinesisMock.listShards(any(ListShardsRequest.class)))
|
||||
.thenReturn(new ListShardsResult().withShards(new Shard()));
|
||||
ProducerDestination destination = provisioner.provisionProducerDestination(name,
|
||||
extendedProducerProperties);
|
||||
|
||||
verify(amazonKinesisMock).listShards(any(ListShardsRequest.class));
|
||||
|
||||
assertThat(destination.getName()).isEqualTo(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvisionConsumerSuccessfulWithExistingStream() {
|
||||
AmazonKinesis amazonKinesisMock = mock(AmazonKinesis.class);
|
||||
KinesisBinderConfigurationProperties binderProperties = new KinesisBinderConfigurationProperties();
|
||||
KinesisStreamProvisioner provisioner = new KinesisStreamProvisioner(
|
||||
amazonKinesisMock, binderProperties);
|
||||
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> extendedConsumerProperties =
|
||||
new ExtendedConsumerProperties<>(
|
||||
new KinesisConsumerProperties());
|
||||
|
||||
String name = "test-stream";
|
||||
String group = "test-group";
|
||||
|
||||
when(amazonKinesisMock.listShards(any(ListShardsRequest.class)))
|
||||
.thenReturn(new ListShardsResult().withShards(new Shard()));
|
||||
|
||||
ConsumerDestination destination = provisioner.provisionConsumerDestination(name,
|
||||
group, extendedConsumerProperties);
|
||||
|
||||
verify(amazonKinesisMock).listShards(any(ListShardsRequest.class));
|
||||
|
||||
assertThat(destination.getName()).isEqualTo(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvisionConsumerExistingStreamUpdateShards() {
|
||||
AmazonKinesis amazonKinesisMock = mock(AmazonKinesis.class);
|
||||
ArgumentCaptor<UpdateShardCountRequest> updateShardCaptor = ArgumentCaptor
|
||||
.forClass(UpdateShardCountRequest.class);
|
||||
String name = "test-stream";
|
||||
String group = "test-group";
|
||||
int targetShardCount = 2;
|
||||
KinesisBinderConfigurationProperties binderProperties = new KinesisBinderConfigurationProperties();
|
||||
binderProperties.setMinShardCount(targetShardCount);
|
||||
binderProperties.setAutoAddShards(true);
|
||||
KinesisStreamProvisioner provisioner = new KinesisStreamProvisioner(
|
||||
amazonKinesisMock, binderProperties);
|
||||
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> extendedConsumerProperties =
|
||||
new ExtendedConsumerProperties<>(
|
||||
new KinesisConsumerProperties());
|
||||
|
||||
DescribeStreamResult describeOriginalStream = describeStreamResultWithShards(
|
||||
Collections.singletonList(new Shard()));
|
||||
|
||||
DescribeStreamResult describeUpdatedStream = describeStreamResultWithShards(
|
||||
Arrays.asList(new Shard(), new Shard()));
|
||||
|
||||
when(amazonKinesisMock.describeStream(any(DescribeStreamRequest.class)))
|
||||
.thenReturn(describeOriginalStream).thenReturn(describeUpdatedStream);
|
||||
|
||||
when(amazonKinesisMock.listShards(any(ListShardsRequest.class)))
|
||||
.thenReturn(new ListShardsResult().withShards(new Shard()))
|
||||
.thenReturn(new ListShardsResult().withShards(new Shard(), new Shard()));
|
||||
|
||||
provisioner.provisionConsumerDestination(name, group, extendedConsumerProperties);
|
||||
|
||||
verify(amazonKinesisMock, times(1)).updateShardCount(updateShardCaptor.capture());
|
||||
|
||||
assertThat(updateShardCaptor.getValue().getStreamName()).isEqualTo(name);
|
||||
assertThat(updateShardCaptor.getValue().getScalingType())
|
||||
.isEqualTo(ScalingType.UNIFORM_SCALING.name());
|
||||
assertThat(updateShardCaptor.getValue().getTargetShardCount())
|
||||
.isEqualTo(targetShardCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvisionProducerSuccessfulWithNewStream() {
|
||||
AmazonKinesis amazonKinesisMock = mock(AmazonKinesis.class);
|
||||
KinesisBinderConfigurationProperties binderProperties = new KinesisBinderConfigurationProperties();
|
||||
KinesisStreamProvisioner provisioner = new KinesisStreamProvisioner(
|
||||
amazonKinesisMock, binderProperties);
|
||||
ExtendedProducerProperties<KinesisProducerProperties> extendedProducerProperties =
|
||||
new ExtendedProducerProperties<>(
|
||||
new KinesisProducerProperties());
|
||||
|
||||
String name = "test-stream";
|
||||
Integer shards = 1;
|
||||
|
||||
when(amazonKinesisMock.listShards(any(ListShardsRequest.class)))
|
||||
.thenThrow(new ResourceNotFoundException("I got nothing"))
|
||||
.thenReturn(new ListShardsResult().withShards(new Shard()));
|
||||
|
||||
|
||||
when(amazonKinesisMock.createStream(name, shards))
|
||||
.thenReturn(new CreateStreamResult());
|
||||
|
||||
when(amazonKinesisMock.describeStream(name))
|
||||
.thenReturn(new DescribeStreamResult()
|
||||
.withStreamDescription(new StreamDescription()
|
||||
.withStreamStatus(StreamStatus.ACTIVE)));
|
||||
|
||||
ProducerDestination destination = provisioner.provisionProducerDestination(name,
|
||||
extendedProducerProperties);
|
||||
|
||||
verify(amazonKinesisMock)
|
||||
.listShards(any(ListShardsRequest.class));
|
||||
|
||||
verify(amazonKinesisMock).createStream(name, shards);
|
||||
|
||||
assertThat(destination.getName()).isEqualTo(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvisionProducerUpdateShards() {
|
||||
AmazonKinesis amazonKinesisMock = mock(AmazonKinesis.class);
|
||||
ArgumentCaptor<UpdateShardCountRequest> updateShardCaptor = ArgumentCaptor
|
||||
.forClass(UpdateShardCountRequest.class);
|
||||
String name = "test-stream";
|
||||
String group = "test-group";
|
||||
int targetShardCount = 2;
|
||||
KinesisBinderConfigurationProperties binderProperties = new KinesisBinderConfigurationProperties();
|
||||
binderProperties.setMinShardCount(targetShardCount);
|
||||
binderProperties.setAutoAddShards(true);
|
||||
KinesisStreamProvisioner provisioner = new KinesisStreamProvisioner(
|
||||
amazonKinesisMock, binderProperties);
|
||||
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> extendedConsumerProperties =
|
||||
new ExtendedConsumerProperties<>(
|
||||
new KinesisConsumerProperties());
|
||||
|
||||
DescribeStreamResult describeOriginalStream = describeStreamResultWithShards(
|
||||
Collections.singletonList(new Shard()));
|
||||
|
||||
DescribeStreamResult describeUpdatedStream = describeStreamResultWithShards(
|
||||
Arrays.asList(new Shard(), new Shard()));
|
||||
|
||||
when(amazonKinesisMock.describeStream(any(DescribeStreamRequest.class)))
|
||||
.thenReturn(describeOriginalStream).thenReturn(describeUpdatedStream);
|
||||
|
||||
when(amazonKinesisMock.listShards(any(ListShardsRequest.class)))
|
||||
.thenReturn(new ListShardsResult().withShards(new Shard()))
|
||||
.thenReturn(new ListShardsResult().withShards(new Shard(), new Shard()));
|
||||
|
||||
provisioner.provisionConsumerDestination(name, group, extendedConsumerProperties);
|
||||
|
||||
verify(amazonKinesisMock, times(1)).updateShardCount(updateShardCaptor.capture());
|
||||
assertThat(updateShardCaptor.getValue().getStreamName()).isEqualTo(name);
|
||||
assertThat(updateShardCaptor.getValue().getScalingType())
|
||||
.isEqualTo(ScalingType.UNIFORM_SCALING.name());
|
||||
assertThat(updateShardCaptor.getValue().getTargetShardCount())
|
||||
.isEqualTo(targetShardCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvisionConsumerSuccessfulWithNewStream() {
|
||||
AmazonKinesis amazonKinesisMock = mock(AmazonKinesis.class);
|
||||
KinesisBinderConfigurationProperties binderProperties = new KinesisBinderConfigurationProperties();
|
||||
KinesisStreamProvisioner provisioner = new KinesisStreamProvisioner(
|
||||
amazonKinesisMock, binderProperties);
|
||||
int instanceCount = 1;
|
||||
int concurrency = 1;
|
||||
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> extendedConsumerProperties =
|
||||
new ExtendedConsumerProperties<>(
|
||||
new KinesisConsumerProperties());
|
||||
extendedConsumerProperties.setInstanceCount(instanceCount);
|
||||
extendedConsumerProperties.setConcurrency(concurrency);
|
||||
|
||||
String name = "test-stream";
|
||||
String group = "test-group";
|
||||
|
||||
when(amazonKinesisMock.listShards(any(ListShardsRequest.class)))
|
||||
.thenThrow(new ResourceNotFoundException("I got nothing"))
|
||||
.thenReturn(new ListShardsResult().withShards(new Shard()));
|
||||
|
||||
when(amazonKinesisMock.createStream(name, instanceCount * concurrency))
|
||||
.thenReturn(new CreateStreamResult());
|
||||
|
||||
when(amazonKinesisMock.describeStream(name))
|
||||
.thenReturn(new DescribeStreamResult()
|
||||
.withStreamDescription(new StreamDescription()
|
||||
.withStreamStatus(StreamStatus.ACTIVE)));
|
||||
|
||||
ConsumerDestination destination = provisioner.provisionConsumerDestination(name,
|
||||
group, extendedConsumerProperties);
|
||||
|
||||
verify(amazonKinesisMock, times(1))
|
||||
.listShards(any(ListShardsRequest.class));
|
||||
|
||||
verify(amazonKinesisMock).createStream(name, instanceCount * concurrency);
|
||||
|
||||
assertThat(destination.getName()).isEqualTo(name);
|
||||
}
|
||||
|
||||
private static DescribeStreamResult describeStreamResultWithShards(
|
||||
List<Shard> shards) {
|
||||
return new DescribeStreamResult().withStreamDescription(new StreamDescription()
|
||||
.withShards(shards).withStreamStatus(StreamStatus.ACTIVE)
|
||||
.withHasMoreShards(Boolean.FALSE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvisionConsumerResourceNotFoundException() {
|
||||
AmazonKinesis amazonKinesisMock = mock(AmazonKinesis.class);
|
||||
KinesisBinderConfigurationProperties binderProperties = new KinesisBinderConfigurationProperties();
|
||||
binderProperties.setAutoCreateStream(false);
|
||||
KinesisStreamProvisioner provisioner = new KinesisStreamProvisioner(
|
||||
amazonKinesisMock, binderProperties);
|
||||
int instanceCount = 1;
|
||||
int concurrency = 1;
|
||||
|
||||
ExtendedConsumerProperties<KinesisConsumerProperties> extendedConsumerProperties =
|
||||
new ExtendedConsumerProperties<>(
|
||||
new KinesisConsumerProperties());
|
||||
extendedConsumerProperties.setInstanceCount(instanceCount);
|
||||
extendedConsumerProperties.setConcurrency(concurrency);
|
||||
|
||||
String name = "test-stream";
|
||||
String group = "test-group";
|
||||
|
||||
when(amazonKinesisMock.listShards(any(ListShardsRequest.class)))
|
||||
.thenThrow(new ResourceNotFoundException("Stream not found"));
|
||||
|
||||
assertThatThrownBy(() -> provisioner.provisionConsumerDestination(name, group,
|
||||
extendedConsumerProperties))
|
||||
.isInstanceOf(ProvisioningException.class)
|
||||
.hasMessageContaining(
|
||||
"The stream [test-stream] was not found and auto creation is disabled.")
|
||||
.hasCauseInstanceOf(ResourceNotFoundException.class);
|
||||
|
||||
verify(amazonKinesisMock, times(1))
|
||||
.listShards(any(ListShardsRequest.class));
|
||||
|
||||
verify(amazonKinesisMock, never()).createStream(name,
|
||||
instanceCount * concurrency);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||
<Pattern>
|
||||
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
|
||||
</Pattern>
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
<logger name="org.springframework.cloud.stream.binder.kinesis" level="debug"/>
|
||||
<logger name="org.springframework.integration" level="warn"/>
|
||||
|
||||
<root level="warn">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
@@ -16,6 +16,7 @@
|
||||
<modules>
|
||||
<module>kafka-binder</module>
|
||||
<module>rabbit-binder</module>
|
||||
<module>kinesis-binder</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -88,6 +88,9 @@
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>spring</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
|
||||
Reference in New Issue
Block a user