Compare commits

..

No commits in common. "main" and "jetcd-0.6.1" have entirely different histories.

220 changed files with 3688 additions and 5720 deletions

View File

@ -6,17 +6,13 @@ labels: ''
assignees: ''
---
**Versions**
- etcd: _add the etcd version here_
- jetcd: _add the jetcd version here_
- java: _add the java version here_
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps or reproducer to reproduce the behavior in a form of a unit test.
This section *must* be provided, if not, the issue may not get attention since the maintainers have very limited capacity.
Steps or reproducer to reproduce the behavior.
This section *must* be provided, if not, the issue may not get attention.
**Expected behavior**
A clear and concise description of what you expected to happen.

View File

@ -1,18 +1,11 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: daily
time: "19:30"
timezone: Europe/Paris
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: daily
time: "19:30"
timezone: Europe/Paris
labels:
- area/dependencies
ignore:
- dependency-name: com.google.protobuf:protoc
- package-ecosystem: maven
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
ignore:
- dependency-name: org.codehaus.plexus:plexus-compiler-javac-errorprone
versions:
- "> 2.8.6, < 2.9"

View File

@ -1,76 +0,0 @@
#
# Copyright 2016-2023 The jetcd 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.
#
name: Build and Publish (Snapshot)
concurrency:
group: ${{ github.workflow }}-${{ github.sha }}
cancel-in-progress: true
on:
push:
branches:
- main
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
jobs:
checks:
uses: ./.github/workflows/checks.yml
build:
needs:
- checks
strategy:
matrix:
java-version:
- 11
- 17
- 21
etcd:
- quay.io/coreos/etcd:v3.5.21
- quay.io/coreos/etcd:v3.6.0
uses: ./.github/workflows/build.yml
with:
javaVersion: "${{ matrix.java-version }}"
etcdImage: "${{ matrix.etcd }}"
publish:
runs-on: ubuntu-latest
needs:
- build
steps:
- name: 'Checkout'
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 'Set Up Java'
uses: actions/setup-java@v4
with:
java-version: 11
distribution: 'temurin'
cache: 'gradle'
- name: 'Collect Info'
run: |
./gradlew currentVersion
- name: 'Publish Snapshot'
if: github.event_name != 'schedule'
env:
NEXUS_USERNAME: ${{ secrets.OSSRH_USERNAME }}
NEXUS_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
run: |
./gradlew publishToSonatype -Prelease.forceSnapshot

46
.github/workflows/build-master.yml vendored Normal file
View File

@ -0,0 +1,46 @@
#
# Copyright 2016-2021 The jetcd 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.
#
name: Build and Publish
concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
on:
push:
branches:
- master
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set Up Java
uses: actions/setup-java@v2
with:
java-version: 11
distribution: 'temurin'
- name: Build Project
env:
OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
run: |
./gradlew spotlessCheck
./gradlew publishToSonatype

View File

@ -1,5 +1,5 @@
#
# Copyright 2016-2023 The jetcd authors
# Copyright 2016-2021 The jetcd authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -17,35 +17,27 @@
name: Build PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
permissions:
contents: read
on:
pull_request:
branches:
- main
- master
workflow_dispatch:
jobs:
checks:
uses: ./.github/workflows/checks.yml
build:
needs:
- checks
strategy:
matrix:
java-version:
- 11
- 17
- 21
etcd:
- quay.io/coreos/etcd:v3.5.21
- quay.io/coreos/etcd:v3.6.0
uses: ./.github/workflows/build.yml
with:
javaVersion: "${{ matrix.java-version }}"
etcdImage: "${{ matrix.etcd }}"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set Up Java
uses: actions/setup-java@v2
with:
java-version: 11
distribution: 'temurin'
- name: Build Project
run: |
./gradlew spotlessCheck
./gradlew test

View File

@ -1,33 +0,0 @@
name: Build
on:
workflow_call:
inputs:
javaVersion:
required: true
type: string
etcdImage:
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 'Checkout'
uses: actions/checkout@v4
- name: 'Set Up Java'
uses: actions/setup-java@v4
with:
java-version: ${{ inputs.javaVersion }}
distribution: 'temurin'
cache: 'gradle'
- name: 'Build Project'
env:
ETCD_IMAGE: ${{ inputs.etcdImage }}
run: |
export TC_USER="$(id -u):$(id -g)"
echo "tc user -> $TC_USER"
./gradlew test --info

View File

@ -1,37 +0,0 @@
name: Checks
on:
workflow_call:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: 'Checkout'
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 'Set Up Java'
uses: actions/setup-java@v4
with:
java-version: 11
distribution: 'temurin'
cache: 'gradle'
- name: 'Run check task'
run: |
./gradlew check -x test
- name: 'Run check script'
run: |
bash ./.github/workflows/scripts/precheck.sh
shell: bash
deps:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: 'Checkout'
uses: actions/checkout@v4
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4

View File

@ -1,55 +0,0 @@
#!/usr/bin/env bash
failed=0
# Check trailing whitespace
files=$(find . -type f \
-not -path "./.git/*" \
-not -path "*/.gradle/*" \
-not -path "*/.idea/*" \
-not -path "*/.vscode/*" \
-not -path "*/build/*" \
-not -path "*/out/*" \
-not -path "*/bin/*" \
-not -name "*.jar" \
-not -name "*.java" \
-exec grep -E -l " +$" {} \;)
count=0
for file in $files; do
((count++))
echo "$file"
done
if [ $count -ne 0 ]; then
failed=1
echo "Error: trailing whitespace(s) in the above $count file(s)"
fi
# Check newline
files=$(find . -type f -size +0c \
-not -path "./.git/*" \
-not -path "*/.gradle/*" \
-not -path "*/.idea/*" \
-not -path "*/.vscode/*" \
-not -path "*/build/*" \
-not -path "*/out/*" \
-not -path "*/bin/*" \
-not -name "*.jar" \
-not -name "*.java" \
-exec bash -c 'if [[ $(tail -c1 "$0" | wc -l) -eq 0 ]]; then echo "$0"; fi' {} \;)
count=0
for file in $files; do
((count++))
echo "$file"
done
if [ $count -ne 0 ]; then
failed=1
echo "Error: no newline in the above $count file(s)"
fi
exit $failed

View File

@ -1,5 +1,5 @@
#
# Copyright 2016-2023 The jetcd authors
# Copyright 2016-2021 The jetcd authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -24,19 +24,18 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60
days-before-close: 7
only-labels: 'waiting-for-feedbacks'
stale-issue-label: stale
exempt-issue-labels: never-stale
exempt-issue-label: never-stale
stale-pr-label: stale
exempt-pr-labels: never-stale
exempt-pr-label: never-stale
stale-issue-message: |
This issue is stale because it has been open 60 days with no activity.
Remove stale label or comment or this will be closed in 7 days.
stale-pr-message: |
This PR is stale because it has been open 60 days with no activity.
Remove stale label or comment or this will be closed in 7 days.
Remove stale label or comment or this will be closed in 7 days.

1
.gitignore vendored
View File

@ -15,4 +15,3 @@ logs/
.sdkmanrc
build/
bin/
out/

View File

@ -4,7 +4,7 @@ jetcd is Apache 2.0 licensed and accepts contributions via GitHub pull requests.
# Email and chat
- Email: [etcd-dev](https://groups.google.com/g/etcd-dev)
- Email: [etcd-dev](https://groups.google.com/forum/?hl=en#!forum/etcd-dev)
- IRC: #[etcd](irc://irc.freenode.org:6667/#etcd) IRC channel on freenode.org
## Getting started
@ -25,7 +25,7 @@ This is a rough outline of what a contributor's workflow looks like:
- Make sure commit messages are in the proper format (see below).
- Push changes in a topic branch to a personal fork of the repository.
- Submit a pull request to etcd-io/jetcd.
- The PR must receive a LGTM from at least one maintainer found in the [OWNERS](https://github.com/etcd-io/jetcd/blob/main/OWNERS) file.
- The PR must receive a LGTM from at least one maintainer found in the [MAINTAINERS](https://github.com/etcd-io/etcd/blob/master/MAINTAINERS) file.
Thanks for contributing!
@ -40,7 +40,7 @@ Please follow this style to make jetcd easy to review, maintain, and develop.
To make sure CI checks would pass please run
```bash
./gradlew spotlessApply
./mvnw license:format
```
and including any changes in PR before opening it.

5
MAINTAINERS Normal file
View File

@ -0,0 +1,5 @@
Fanmin Shi <fanmin.shi@coreos.com> (@fanminshi) pkg:*
Luca Burgazzoli <lburgazzoli@gmail.com> (@lburgazzoli) pkg:*
Xiang Li <xiang.li@coreos.com> (@xiang90) pkg:*
Anthony Romano <anthony.romano@coreos.com> (@heyitsanthony) pkg:*

6
OWNERS
View File

@ -1,6 +0,0 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- fanminshi # Fanmin Shi <fanmin.shi@coreos.com>
- lburgazzoli # Luca Burgazzoli <lburgazzoli@gmail.com>
- vorburger # Michael Vorburger <mike@vorburger.ch>

View File

@ -1,5 +1,5 @@
# jetcd - A Java Client for etcd
[![Build Status](https://github.com/etcd-io/jetcd/actions/workflows/build-main.yml/badge.svg)](https://github.com/etcd-io/jetcd/actions)
[![Build Status](https://github.com/etcd-io/jetcd/actions/workflows/build-master.yml/badge.svg)](https://github.com/etcd-io/jetcd/actions)
[![License](https://img.shields.io/badge/Licence-Apache%202.0-blue.svg?style=flat-square)](http://www.apache.org/licenses/LICENSE-2.0.html)
[![Maven Central](https://img.shields.io/maven-central/v/io.etcd/jetcd-core.svg?style=flat-square)](https://search.maven.org/#search%7Cga%7C1%7Cio.etcd)
[![Javadocs](http://www.javadoc.io/badge/io/etcd/jetcd-core.svg)](https://javadoc.io/doc/io.etcd/jetcd-core)
@ -23,13 +23,13 @@ Java 11 or above is required.
</dependency>
```
Development snapshots are available in [Sonatypes's snapshot repository](https://oss.sonatype.org/content/repositories/snapshots/io/etcd/).
Development snapshots are available in [Sonatypes's snapshot repository](https://oss.sonatype.org/content/repositories/snapshots/io/etcd).
### Gradle
```
dependencies {
implementation "io.etcd:jetcd-core:$jetcd-version"
compile "io.etcd:jetcd-core:$jetcd-version"
}
```
@ -63,7 +63,6 @@ GetResponse response = getFuture.get();
// delete the key
kvClient.delete(key).get();
```
To build one ssl secured client, refer to [secured client config](docs/SslConfig.md).
For full etcd v3 API, plesase refer to the [official API documentation](https://etcd.io/docs/current/learning/api/).
@ -80,7 +79,7 @@ import io.etcd.jetcd.Client;
import io.etcd.jetcd.test.EtcdClusterExtension;
import org.junit.jupiter.api.extension.RegisterExtension;
@RegisterExtension
@RegisterExtension
public static final EtcdClusterExtension cluster = EtcdClusterExtension.builder()
.withNodes(1)
.build();
@ -99,7 +98,7 @@ The current major version is zero (0.y.z). Anything may change at any time. The
## Build from source
The project can be built with [Gradle](https://gradle.org/):
The project can be built with [Apache Maven](https://maven.apache.org/):
```
./gradlew compileJava
@ -111,7 +110,7 @@ The project is tested against a three node `etcd` setup started with the Launche
```sh
$ ./gradlew test
```
````
### Troubleshooting
@ -119,7 +118,7 @@ It recommmonds building the project before running tests so that you have artifa
## Contact
* Mailing list: [etcd-dev](https://groups.google.com/g/etcd-dev)
* Mailing list: [etcd-dev](https://groups.google.com/forum/?hl=en#!forum/etcd-dev)
## Contributing
@ -128,5 +127,3 @@ See [CONTRIBUTING](https://github.com/etcd-io/jetcd/blob/master/CONTRIBUTING.md)
## License
jetcd is under the Apache 2.0 license. See the [LICENSE](https://github.com/etcd-io/jetcd/blob/master/LICENSE) file for details.

View File

@ -19,13 +19,13 @@ buildscript {
gradlePluginPortal()
}
dependencies {
classpath "com.github.ben-manes:gradle-versions-plugin:${libs.versions.versionsPlugin.get()}"
classpath "com.diffplug.spotless:spotless-plugin-gradle:${libs.versions.spotlessPlugin.get()}"
classpath "net.ltgt.gradle:gradle-errorprone-plugin:${libs.versions.errorPronePlugin.get()}"
classpath "io.github.gradle-nexus:publish-plugin:${libs.versions.nexusPublishPlugin.get()}"
classpath "com.adarshr:gradle-test-logger-plugin:${libs.versions.testLoggerPlugin.get()}"
classpath "pl.allegro.tech.build:axion-release-plugin:${libs.versions.axionReleasePlugin.get()}"
classpath "org.gradle:test-retry-gradle-plugin:${libs.versions.testRetryPlugin.get()}"
classpath "com.github.ben-manes:gradle-versions-plugin:$versionsPluginVersion"
classpath "com.diffplug.spotless:spotless-plugin-gradle:$spotlessPluginVersion"
classpath "net.ltgt.gradle:gradle-errorprone-plugin:$errorPronePluginVersion"
classpath "io.github.gradle-nexus:publish-plugin:$nexusPublishPluginVersion"
classpath "com.adarshr:gradle-test-logger-plugin:$testLoggerPluginVersion"
classpath "se.ascp.gradle:gradle-versions-filter:$versionsFilterPluginVersion"
classpath "pl.allegro.tech.build:axion-release-plugin:$axionReleasePluginVersion"
}
}
@ -49,52 +49,25 @@ subprojects {
apply from: "${rootProject.projectDir}/gradle/publishing-release-tasks.gradle"
apply plugin: 'java-library'
apply plugin: 'com.adarshr.test-logger'
apply plugin: 'org.gradle.test-retry'
apply plugin: "com.adarshr.test-logger"
configurations.api {
exclude group: 'com.google.code.gson', module: 'gson'
exclude group: 'com.google.android', module: 'annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
}
compileJava {
options.release = 11
java {
toolchain {
languageVersion = JavaLanguageVersion.of(11)
}
}
test {
useJUnitPlatform()
maxParallelForks = Runtime.runtime.availableProcessors() ?: 1
retry {
maxRetries = 1
maxFailures = 5
}
}
testing {
suites {
test {
useJUnitJupiter()
}
}
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
}
testlogger {
theme 'mocha-parallel'
showStandardStreams false
}
tasks.register('allDeps', DependencyReportTask)
tasks.withType(AbstractArchiveTask).configureEach {
preserveFileTimestamps = false
reproducibleFileOrder = true
}
}

View File

@ -1,3 +1,61 @@
## etcd Community Code of Conduct
## CoreOS Community Code of Conduct
etcd follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
### Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of
fostering an open and welcoming community, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating
documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free
experience for everyone, regardless of level of experience, gender, gender
identity and expression, sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing others' private information, such as physical or electronic addresses, without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct. By adopting this Code of Conduct,
project maintainers commit themselves to fairly and consistently applying these
principles to every aspect of managing this project. Project maintainers who do
not follow or enforce the Code of Conduct may be permanently removed from the
project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting a project maintainer, Brandon Philips
<brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.
This Code of Conduct is adapted from the Contributor Covenant
(http://contributor-covenant.org), version 1.2.0, available at
http://contributor-covenant.org/version/1/2/0/
### CoreOS Events Code of Conduct
CoreOS events are working conferences intended for professional networking and
collaboration in the CoreOS community. Attendees are expected to behave
according to professional standards and in accordance with their employers
policies on appropriate workplace behavior.
While at CoreOS events or related social networking opportunities, attendees
should not engage in discriminatory or offensive speech or actions including
but not limited to gender, sexuality, race, age, disability, or religion.
Speakers should be especially aware of these concerns.
CoreOS does not condone any statements by speakers contrary to these standards.
CoreOS reserves the right to deny entrance and/or eject from an event (without
refund) any individual found to be engaging in discriminatory or offensive
speech or actions.
Please bring any concerns to the immediate attention of designated on-site
staff, Brandon Philips <brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.

View File

@ -26,3 +26,6 @@ for (KeyValue kv : response.getKvs()) {
);
}
----

View File

@ -46,7 +46,7 @@ The Lease interface provides methods to grant, revoke, and keepalive leases.
1. This function is called periodically by `keepAliveSchedule`.
2. The lease may expire as:
* The etcd respond overtime.
* The client fails to send request in time.
* The client faile to send request in time.
3. It will scan the keepAlives map and find the leases that requires keepAlive requests to send based on its nextKeepAliveTime.
4. Send request to the StreamObserver for these leases.
@ -66,4 +66,4 @@ The Lease interface provides methods to grant, revoke, and keepalive leases.
1. The StreamObserver is the stream etcd uses to send responses.
2. The gRPC client calls onNext when etcd server delivers a response.
3. The onNext function will call `processKeepAliveRespond`.
3. The onNext function will call `processKeepAliveRespond`.

View File

@ -1,21 +0,0 @@
= Release
+
[source,shell]
----
$ ./gradlew currentVersion
Project version: 0.7.5-SNAPSHOT
$ ./gradlew release
$ git tag
jetcd-0.7.5
$ ./gradlew currentVersion
Project version: 0.7.5
$ ./gradlew publish
published jetcd-0.7.5 release version
$ ./gradlew markNextVersion -Prelease.version=0.7.6
----

View File

@ -1,36 +0,0 @@
# How to Build Jectd Client for One TLS Secured Etcd Cluster
# prepare certification files
If your etcd cluster is installed using [etcdadm](https://github.com/kubernetes-sigs/etcdadm), you are likely to find
certification files in the path `/etc/etcd/pki/`: `ca.crt`, `etcdctl-etcd-client.key`, `etcdctl-etcd-client.crt`.
If your etcd cluster is the builtin etcd in one kubernetes cluster(
using [kubeadm](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/setup-ha-etcd-with-kubeadm/)),
you can find the same files in `/etc/kubernetes/pki/etcd/`: `ca.crt`, `healthcheck-client.crt`, `healthcheck-client.key`.
Because `SslContextBuilder` only support a PKCS#8 private key file in PEM format, convert `etcdctl-etcd-client.key`
to `etcdctl-etcd-client.key.pem` according
to [netty SslContextBuilder doc](https://netty.io/wiki/sslcontextbuilder-and-private-key.html).
# build jetcd client
```
File cert = new File("ca.crt");
File keyCertChainFile = new File("etcdctl-etcd-client.crt");
File keyFile = new File("etcdctl-etcd-client.key.pem");
SslContext context = GrpcSslContexts.forClient()
.trustManager(cert)
.keyManager(keyCertChainFile, keyFile)
.build();
Client client = Client.builder()
.endpoints("https://10.168.168.66:2379")
.sslContext(context)
.build();
client.getClusterClient().listMember().get().getMembers().forEach(member -> {
logger.info("member: {}", member);
});
```

View File

@ -12,15 +12,15 @@ The Watch provide methods to watch on a key interval and cancel a watcher. If th
4. Cancel watch request, the etcd client should process watch cancellation and filter all the notification after cancellation request.
5. The watch client should be able to make a progress notify request that propagates the latest revision number to all watches.
5. The watch client should be able to make a progress notify request that propagates the latest revision number to all watches
# Implementation
The etcd client process watch request with [watch function](#watch-function), process notification with [processEvents function](#processevents-function), process resume with [resume function](#resume-function), process cancel with [cancelWatch function](#cancelwatch-function) and request progress with [requestProgress function](#requestProgress-function).
The etcd client process watch request with [watch function](#watch-function), process notification with [processEvents function](#processevents-function) , process resume with [resume function](#resume-function), process cancel with [cancelWatch function](#cancelwatch-function) and request progress with [requestProgress function](#requestProgress-function).
## watch function
Watch watches on a key interval.
Watch watches on a key interval.
1. Send create request to [requestStream](#requeststream-instance).
2. If the watch is create successfully, the `onCreate` will be called and the ListenableFuture task will be completed.
@ -116,4 +116,4 @@ Process cancel response from etcd server.
Resume all the the watchers on new requestStream.
1. Build new watch creation request for old watcher with last revision + 1.
2. Call `watch` function with the new watch creation request.
2. Call `watch` function with the new watch creation request.

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2016-2023 The jetcd authors
Copyright 2016-2021 The jetcd authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -271,3 +271,4 @@
<setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="CHECKSTYLE:ON"/>
</profile>
</profiles>

View File

@ -10,4 +10,4 @@ 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.
limitations under the License.

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2016-2023 The jetcd authors
Copyright 2016-2021 The jetcd authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -23,8 +23,6 @@
<description>PMD Rules that govern static code analysis for jetcd</description>
<exclude-pattern>.*/io/etcd/jetcd/api/.*</exclude-pattern>
<rule ref="category/java/bestpractices.xml/AvoidUsingHardCodedIP"/>
<rule ref="category/java/bestpractices.xml/UnusedFormalParameter"/>
<rule ref="category/java/bestpractices.xml/UnusedLocalVariable"/>
@ -44,7 +42,6 @@
<rule ref="category/java/codestyle.xml/UselessQualifiedThis"/>
<rule ref="category/java/codestyle.xml/ControlStatementBraces"/>
<rule ref="category/java/codestyle.xml/UnnecessaryImport"/>
<rule ref="category/java/codestyle.xml/EmptyControlStatement"/>
<rule ref="category/java/design.xml/FinalFieldCouldBeStatic">
<priority>4</priority>
@ -74,6 +71,15 @@
</properties>
</rule>
<rule ref="category/java/errorprone.xml/EmptyFinallyBlock"/>
<rule ref="category/java/errorprone.xml/EmptyIfStmt"/>
<rule ref="category/java/errorprone.xml/EmptyInitializer"/>
<rule ref="category/java/errorprone.xml/EmptyStatementBlock"/>
<rule ref="category/java/errorprone.xml/EmptyStatementNotInLoop"/>
<rule ref="category/java/errorprone.xml/EmptySwitchStatements"/>
<rule ref="category/java/errorprone.xml/EmptySynchronizedBlock"/>
<rule ref="category/java/errorprone.xml/EmptyTryBlock"/>
<rule ref="category/java/errorprone.xml/EmptyWhileStmt"/>
<rule ref="category/java/errorprone.xml/JumbledIncrementer"/>
<rule ref="category/java/errorprone.xml/MisplacedNullCheck"/>
<rule ref="category/java/errorprone.xml/OverrideBothEqualsAndHashcode"/>
@ -90,4 +96,4 @@
<rule ref="category/java/performance.xml/BigIntegerInstantiation"/>
<rule ref="category/java/bestpractices.xml/PrimitiveWrapperInstantiation" />
</ruleset>
</ruleset>

View File

@ -1,10 +1,30 @@
#
# project
#
gitProject = https://github.com/etcd-io/jetcd
gitURL = git@github.com/etcd-io/jetcd.git
gitProject = 'https://github.com/etcd-io/jetcd'
gitURL = 'git@github.com/etcd-io/jetcd.git'
#
# dependencies
#
grpcVersion = 1.42.1
protocVersion = 3.17.2
#
# plugins
#
versionsPluginVersion = 0.39.0
versionsFilterPluginVersion = 0.1.10
errorPronePluginVersion = 2.0.2
spotlessPluginVersion = 6.0.0
shadowPluginVersion = 7.1.0
testLoggerPluginVersion = 3.1.0
protobufPluginVersion = 0.8.18
nexusPublishPluginVersion = 1.1.0
axionReleasePluginVersion = 1.13.6
#
# gradle
#
org.gradle.parallel = true

View File

@ -1,46 +1,38 @@
[versions]
grpc = "1.74.0"
log4j = "2.25.1"
mockito = "5.18.0"
slf4j = "2.0.17"
guava = "33.4.8-jre"
assertj = "3.27.3"
junit = "5.13.4"
testcontainers = "1.21.3"
protoc = "3.25.1"
failsafe = "3.3.2"
awaitility = "4.3.0"
commonsIo = "2.20.0"
commonCompress = "1.28.0"
autoService = "1.1.1"
errorprone = "2.30.0"
vertx = "5.0.1"
picocli = "4.7.7"
restAssured = "5.5.5"
grpc = "1.42.1"
log4j = "2.14.1"
mockito = "4.0.0"
slf4j = "1.7.32"
guava = "31.0.1-jre"
javaxAnnotation = "1.3.2"
versionsPlugin = "0.52.0"
errorPronePlugin = "4.0.1"
spotlessPlugin = "6.25.0"
shadowPlugin = "8.1.1"
testLoggerPlugin = "4.0.0"
protobufPlugin = "0.9.5"
nexusPublishPlugin = "2.0.0"
axionReleasePlugin = "1.18.17"
testRetryPlugin = "1.6.2"
spotbugs = "4.5.0"
assertj = "3.21.0"
junit = "5.8.1"
testcontainers = "1.16.2"
protoc = "3.19.1"
failsafe = "2.4.4"
awaitility = "4.1.1"
commonsIo = "2.11.0"
commonCompress = "1.21"
autoService = "1.0.1"
jool = "0.9.14"
jcommander = "1.81"
errorprone = "2.10.0"
[libraries]
slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
javaxAnnotation = { module = "javax.annotation:javax.annotation-api", version.ref = "javaxAnnotation" }
spotbugs = { module = "com.github.spotbugs:spotbugs-annotations", version.ref = "spotbugs" }
assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" }
junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" }
protoc = { module = "com.google.protobuf:protoc", version.ref = "protoc" }
failsafe = { module = "dev.failsafe:failsafe", version.ref = "failsafe" }
failsafe = { module = "net.jodah:failsafe", version.ref = "failsafe" }
awaitility = { module = "org.awaitility:awaitility", version.ref = "awaitility" }
picocli = { module = "info.picocli:picocli", version.ref = "picocli"}
restAssured = { module = "io.rest-assured:rest-assured", version.ref = "restAssured"}
jcommander = { module = "com.beust:jcommander", version.ref = "jcommander"}
jool = { module = "org.jooq:jool-java-8", version.ref = "jool"}
commonsIo = { module = "commons-io:commons-io", version.ref = "commonsIo" }
commonsCompress = { module = "org.apache.commons:commons-compress", version.ref = "commonCompress" }
@ -53,34 +45,23 @@ grpcNetty = { module = "io.grpc:grpc-netty", version.ref = "grpc" }
grpcProtobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpc" }
grpcStub = { module = "io.grpc:grpc-stub", version.ref = "grpc" }
grpcGrpclb = { module = "io.grpc:grpc-grpclb", version.ref = "grpc" }
grpcInprocess = { module = "io.grpc:grpc-inprocess", version.ref = "grpc" }
grpcUtil = { module = "io.grpc:grpc-util", version.ref = "grpc" }
vertxGrpc = { module = "io.vertx:vertx-grpc", version.ref = "vertx" }
log4jApi = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" }
log4jCore = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" }
log4jSlf4j = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" }
log4jSlf4j = { module = "org.apache.logging.log4j:log4j-slf4j-impl", version.ref = "log4j" }
log4j12 = { module = "org.apache.logging.log4j:log4j-1.2-api", version.ref = "log4j" }
autoServiceAnnotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService"}
autoServiceProcessor = { module = "com.google.auto.service:auto-service", version.ref = "autoService"}
javaxAnnotation = { module = "javax.annotation:javax.annotation-api", version.ref = "javaxAnnotation" }
errorprone = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone" }
errorproneAnnotations = { module = "com.google.errorprone:error_prone_annotations", version.ref = "errorprone" }
errorproneJavac = { module = "com.google.errorprone:javac", version = "9+" }
errorproneJavac = { module = "com.google.errorprone:javac", version = "9+181-r4173-1" }
[bundles]
grpc = [ "grpcCore", "grpcNetty", "grpcProtobuf", "grpcStub", "grpcGrpclb", "grpcUtil"]
grpcTest = [ "grpcInprocess"]
log4j = [ "log4jApi", "log4jCore", "log4jSlf4j", "log4j12" ]
mockito = [ "mockitoCore", "mockitoJunit" ]
testing = ["junit", "assertj", "mockitoCore", "mockitoJunit"]
javax = [ "javaxAnnotation" ]
grpc = [ "grpcCore", "grpcNetty", "grpcProtobuf", "grpcStub", "grpcGrpclb" ]
log4j = [ "log4jApi", "log4jCore", "log4jSlf4j", "log4j12" ]
mockito = [ "mockitoCore", "mockitoJunit" ]
testing = ["junit", "assertj", "mockitoCore", "mockitoJunit"]
[plugins]
shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadowPlugin" }
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }

View File

@ -20,10 +20,10 @@ apply plugin: "io.github.gradle-nexus.publish-plugin"
ext {
if (!project.hasProperty('nexusUsername')) {
nexusUsername = "$System.env.NEXUS_USERNAME"
nexusUsername = "$System.env.OSSRH_USERNAME"
}
if (!project.hasProperty('nexusPassword')) {
nexusPassword = "$System.env.NEXUS_PASSWORD"
nexusPassword = "$System.env.OSSRH_PASSWORD"
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2016-2023 The jetcd authors
* Copyright 2016-2020 The jetcd authors
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
@ -22,4 +22,4 @@ task pubblications {
println "Publication $publication.name [$publication.groupId/$publication.artifactId/$publication.version]"
}
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2016-2023 The jetcd authors
* Copyright 2016-2021 The jetcd authors
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
@ -89,12 +89,9 @@ signing {
sign publishing.publications."${project.name}"
}
check.dependsOn javadoc
javadoc {
if(JavaVersion.current().isJava9Compatible()) {
options.addBooleanOption('html5', true)
}
exclude "**/io/etcd/jetcd/api/**"
}

View File

@ -19,7 +19,6 @@ apply plugin: "pmd"
dependencies {
errorprone(libs.errorprone)
errorprone(libs.errorproneAnnotations)
errorproneJavac(libs.errorproneJavac)
}
@ -28,10 +27,9 @@ tasks.withType(JavaCompile).configureEach {
excludedPaths = '.*/generated/.*'
disableWarningsInGeneratedCode = true
}
options.deprecation = true
}
pmd {
consoleOutput = true
ruleSets = [ "${project.rootDir}/etc/pmd-ruleset.xml" ]
ruleSets = [ rootProject.file("etc/pmd-ruleset.xml") ]
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2016-2023 The jetcd authors
* Copyright 2016-2021 The jetcd authors
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
@ -25,4 +25,4 @@ scmVersion {
allprojects {
project.version = scmVersion.version
}
}

View File

@ -24,6 +24,7 @@ spotless {
endWithNewline()
importOrderFile(rootProject.file('etc/eclipse.importorder'))
eclipse().configFile(rootProject.file('etc/eclipse-formatter-config.xml'))
targetExclude("build/generated/**/*.java")
//licenseHeaderFile(rootProject.file('etc/license.txt'))
//licenseHeader 'Copyright $YEAR The jetcd authors'
}
}

View File

@ -15,16 +15,11 @@
*/
apply plugin: 'com.github.ben-manes.versions'
apply plugin: 'se.ascp.gradle.gradle-versions-filter'
dependencyUpdates.configure {
def isNonStable = { String version ->
return ['alpha', 'beta', 'rc', 'cr', 'm', 'ea'].any { keyword -> version.toUpperCase().contains(keyword.toUpperCase())}
}
rejectVersionIf {
isNonStable(it.candidate.version)
}
versionsFilter {
exclusiveQualifiers = ['alpha', 'beta', 'rc', 'cr', 'm', 'ea']
log = false
}
task deps {

Binary file not shown.

View File

@ -1,7 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
networkTimeout=10000
validateDistributionUrl=true
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

47
gradlew vendored
View File

@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -57,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -82,11 +80,13 @@ do
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@ -133,29 +133,22 @@ location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -200,28 +193,18 @@ if "$cygwin" || "$msys" ; then
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

183
gradlew.bat vendored
View File

@ -1,94 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -17,4 +17,6 @@
dependencies {
api libs.slf4j
api libs.guava
api libs.grpcCore
}

View File

@ -1,57 +0,0 @@
/*
* Copyright 2016-2021 The jetcd 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.
*/
package io.etcd.jetcd.common;
import java.util.concurrent.atomic.AtomicBoolean;
public abstract class Service implements AutoCloseable {
private final AtomicBoolean running;
protected Service() {
this.running = new AtomicBoolean();
}
public void start() {
if (this.running.compareAndSet(false, true)) {
doStart();
}
}
public void stop() {
if (this.running.compareAndSet(true, false)) {
doStop();
}
}
public void restart() {
stop();
start();
}
@Override
public void close() {
stop();
}
public boolean isRunning() {
return running.get();
}
protected abstract void doStart();
protected abstract void doStop();
}

View File

@ -31,22 +31,12 @@ import io.grpc.Status;
*/
public enum ErrorCode {
CANCELLED(Status.CANCELLED),
UNKNOWN(Status.UNKNOWN),
INVALID_ARGUMENT(Status.INVALID_ARGUMENT),
DEADLINE_EXCEEDED(Status.DEADLINE_EXCEEDED),
NOT_FOUND(Status.NOT_FOUND),
ALREADY_EXISTS(Status.ALREADY_EXISTS),
PERMISSION_DENIED(Status.PERMISSION_DENIED),
UNAUTHENTICATED(Status.UNAUTHENTICATED),
RESOURCE_EXHAUSTED(Status.RESOURCE_EXHAUSTED),
FAILED_PRECONDITION(Status.FAILED_PRECONDITION),
ABORTED(Status.ABORTED),
OUT_OF_RANGE(Status.OUT_OF_RANGE),
UNIMPLEMENTED(Status.UNIMPLEMENTED),
INTERNAL(Status.INTERNAL),
UNAVAILABLE(Status.UNAVAILABLE),
DATA_LOSS(Status.DATA_LOSS),;
CANCELLED(Status.CANCELLED), UNKNOWN(Status.UNKNOWN), INVALID_ARGUMENT(Status.INVALID_ARGUMENT),
DEADLINE_EXCEEDED(Status.DEADLINE_EXCEEDED), NOT_FOUND(Status.NOT_FOUND), ALREADY_EXISTS(Status.ALREADY_EXISTS),
PERMISSION_DENIED(Status.PERMISSION_DENIED), UNAUTHENTICATED(Status.UNAUTHENTICATED),
RESOURCE_EXHAUSTED(Status.RESOURCE_EXHAUSTED), FAILED_PRECONDITION(Status.FAILED_PRECONDITION), ABORTED(Status.ABORTED),
OUT_OF_RANGE(Status.OUT_OF_RANGE), UNIMPLEMENTED(Status.UNIMPLEMENTED), INTERNAL(Status.INTERNAL),
UNAVAILABLE(Status.UNAVAILABLE), DATA_LOSS(Status.DATA_LOSS),;
private static final Map<Integer, ErrorCode> errorByRpcCode;

View File

@ -16,7 +16,7 @@
package io.etcd.jetcd.common.exception;
import java.util.Objects;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Base exception type for all exceptions produced by the etcd service.
@ -27,7 +27,7 @@ public class EtcdException extends RuntimeException {
EtcdException(ErrorCode code, String message, Throwable cause) {
super(message, cause);
this.code = Objects.requireNonNull(code, "Error code must not be null");
this.code = checkNotNull(code);
}
/**

View File

@ -16,10 +16,9 @@
package io.etcd.jetcd.common.exception;
import java.util.Objects;
import io.grpc.Status;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.Status.fromThrowable;
/**
@ -66,7 +65,7 @@ public final class EtcdExceptionFactory {
}
public static EtcdException toEtcdException(Throwable cause) {
Objects.requireNonNull(cause, "cause can't be null");
checkNotNull(cause, "cause can't be null");
if (cause instanceof EtcdException) {
return (EtcdException) cause;
}
@ -75,7 +74,7 @@ public final class EtcdExceptionFactory {
}
public static EtcdException toEtcdException(Status status) {
Objects.requireNonNull(status, "status can't be null");
checkNotNull(status, "status can't be null");
return fromStatus(status);
}

View File

@ -21,17 +21,17 @@ dependencies {
api libs.slf4j
api libs.guava
api libs.javaxAnnotation
api libs.bundles.grpc
api libs.spotbugs
api libs.failsafe
//compileOnly libs.javaxAnnotation
compileOnly libs.autoServiceAnnotations
api libs.autoServiceAnnotations
annotationProcessor libs.autoServiceProcessor
testImplementation project(':jetcd-launcher')
testImplementation project(':jetcd-test')
testImplementation libs.restAssured
testImplementation libs.awaitility
testImplementation libs.commonsIo
testImplementation libs.bundles.testing

View File

@ -14,9 +14,8 @@
* limitations under the License.
*/
package io.etcd.jetcd.impl;
package io.etcd.jetcd;
import io.etcd.jetcd.Response;
import io.etcd.jetcd.api.ResponseHeader;
public class AbstractResponse<R> implements Response {

View File

@ -14,14 +14,13 @@
* limitations under the License.
*/
package io.etcd.jetcd.impl;
package io.etcd.jetcd;
import java.util.concurrent.CompletableFuture;
import io.etcd.jetcd.Auth;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.api.AuthDisableRequest;
import io.etcd.jetcd.api.AuthEnableRequest;
import io.etcd.jetcd.api.AuthGrpc;
import io.etcd.jetcd.api.AuthRoleAddRequest;
import io.etcd.jetcd.api.AuthRoleDeleteRequest;
import io.etcd.jetcd.api.AuthRoleGetRequest;
@ -35,7 +34,6 @@ import io.etcd.jetcd.api.AuthUserGetRequest;
import io.etcd.jetcd.api.AuthUserGrantRoleRequest;
import io.etcd.jetcd.api.AuthUserListRequest;
import io.etcd.jetcd.api.AuthUserRevokeRoleRequest;
import io.etcd.jetcd.api.VertxAuthGrpc;
import io.etcd.jetcd.auth.AuthDisableResponse;
import io.etcd.jetcd.auth.AuthEnableResponse;
import io.etcd.jetcd.auth.AuthRoleAddResponse;
@ -53,153 +51,120 @@ import io.etcd.jetcd.auth.AuthUserListResponse;
import io.etcd.jetcd.auth.AuthUserRevokeRoleResponse;
import io.etcd.jetcd.auth.Permission;
import com.google.protobuf.ByteString;
import static java.util.Objects.requireNonNull;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Implementation of etcd auth client.
*/
final class AuthImpl extends Impl implements Auth {
final class AuthImpl implements Auth {
private final VertxAuthGrpc.AuthVertxStub stub;
private final AuthGrpc.AuthFutureStub stub;
private final ClientConnectionManager connectionManager;
AuthImpl(ClientConnectionManager connectionManager) {
super(connectionManager);
this.stub = connectionManager.newStub(VertxAuthGrpc::newVertxStub);
this.connectionManager = connectionManager;
this.stub = connectionManager.newStub(AuthGrpc::newFutureStub);
}
@Override
public CompletableFuture<AuthEnableResponse> authEnable() {
AuthEnableRequest enableRequest = AuthEnableRequest.getDefaultInstance();
return completable(
this.stub.authEnable(enableRequest),
AuthEnableResponse::new);
return Util.toCompletableFuture(this.stub.authEnable(enableRequest), AuthEnableResponse::new,
this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<AuthDisableResponse> authDisable() {
AuthDisableRequest disableRequest = AuthDisableRequest.getDefaultInstance();
return completable(
this.stub.authDisable(disableRequest),
AuthDisableResponse::new);
return Util.toCompletableFuture(this.stub.authDisable(disableRequest), AuthDisableResponse::new,
this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<AuthUserAddResponse> userAdd(ByteSequence user, ByteSequence password) {
requireNonNull(user, "user can't be null");
requireNonNull(password, "password can't be null");
checkNotNull(user, "user can't be null");
checkNotNull(password, "password can't be null");
AuthUserAddRequest addRequest = AuthUserAddRequest.newBuilder()
.setNameBytes(ByteString.copyFrom(user.getBytes()))
.setPasswordBytes(ByteString.copyFrom(password.getBytes()))
.build();
return completable(
this.stub.userAdd(addRequest),
AuthUserAddResponse::new);
AuthUserAddRequest addRequest = AuthUserAddRequest.newBuilder().setNameBytes(user.getByteString())
.setPasswordBytes(password.getByteString()).build();
return Util.toCompletableFuture(this.stub.userAdd(addRequest), AuthUserAddResponse::new,
this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<AuthUserDeleteResponse> userDelete(ByteSequence user) {
requireNonNull(user, "user can't be null");
checkNotNull(user, "user can't be null");
AuthUserDeleteRequest deleteRequest = AuthUserDeleteRequest.newBuilder()
.setNameBytes(ByteString.copyFrom(user.getBytes()))
.build();
return completable(
this.stub.userDelete(deleteRequest),
AuthUserDeleteResponse::new);
AuthUserDeleteRequest deleteRequest = AuthUserDeleteRequest.newBuilder().setNameBytes(user.getByteString()).build();
return Util.toCompletableFuture(this.stub.userDelete(deleteRequest), AuthUserDeleteResponse::new,
this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<AuthUserChangePasswordResponse> userChangePassword(ByteSequence user, ByteSequence password) {
requireNonNull(user, "user can't be null");
requireNonNull(password, "password can't be null");
checkNotNull(user, "user can't be null");
checkNotNull(password, "password can't be null");
AuthUserChangePasswordRequest changePasswordRequest = AuthUserChangePasswordRequest.newBuilder()
.setNameBytes(ByteString.copyFrom(user.getBytes()))
.setPasswordBytes(ByteString.copyFrom(password.getBytes()))
.build();
return completable(
this.stub.userChangePassword(changePasswordRequest),
AuthUserChangePasswordResponse::new);
.setNameBytes(user.getByteString()).setPasswordBytes(password.getByteString()).build();
return Util.toCompletableFuture(this.stub.userChangePassword(changePasswordRequest),
AuthUserChangePasswordResponse::new, this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<AuthUserGetResponse> userGet(ByteSequence user) {
requireNonNull(user, "user can't be null");
checkNotNull(user, "user can't be null");
AuthUserGetRequest userGetRequest = AuthUserGetRequest.newBuilder()
.setNameBytes(ByteString.copyFrom(user.getBytes()))
.build();
return completable(
this.stub.userGet(userGetRequest),
AuthUserGetResponse::new);
AuthUserGetRequest userGetRequest = AuthUserGetRequest.newBuilder().setNameBytes(user.getByteString()).build();
return Util.toCompletableFuture(this.stub.userGet(userGetRequest), AuthUserGetResponse::new,
this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<AuthUserListResponse> userList() {
AuthUserListRequest userListRequest = AuthUserListRequest.getDefaultInstance();
return completable(
this.stub.userList(userListRequest),
AuthUserListResponse::new);
return Util.toCompletableFuture(this.stub.userList(userListRequest), AuthUserListResponse::new,
this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<AuthUserGrantRoleResponse> userGrantRole(ByteSequence user, ByteSequence role) {
requireNonNull(user, "user can't be null");
requireNonNull(role, "key can't be null");
checkNotNull(user, "user can't be null");
checkNotNull(role, "key can't be null");
AuthUserGrantRoleRequest userGrantRoleRequest = AuthUserGrantRoleRequest.newBuilder()
.setUserBytes(ByteString.copyFrom(user.getBytes()))
.setRoleBytes(ByteString.copyFrom(role.getBytes()))
.build();
return completable(
this.stub.userGrantRole(userGrantRoleRequest),
AuthUserGrantRoleResponse::new);
AuthUserGrantRoleRequest userGrantRoleRequest = AuthUserGrantRoleRequest.newBuilder().setUserBytes(user.getByteString())
.setRoleBytes(role.getByteString()).build();
return Util.toCompletableFuture(this.stub.userGrantRole(userGrantRoleRequest), AuthUserGrantRoleResponse::new,
this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<AuthUserRevokeRoleResponse> userRevokeRole(ByteSequence user, ByteSequence role) {
requireNonNull(user, "user can't be null");
requireNonNull(role, "key can't be null");
checkNotNull(user, "user can't be null");
checkNotNull(role, "key can't be null");
AuthUserRevokeRoleRequest userRevokeRoleRequest = AuthUserRevokeRoleRequest.newBuilder()
.setNameBytes(ByteString.copyFrom(user.getBytes()))
.setRoleBytes(ByteString.copyFrom(role.getBytes()))
.build();
return completable(
this.stub.userRevokeRole(userRevokeRoleRequest),
AuthUserRevokeRoleResponse::new);
.setNameBytes(user.getByteString()).setRoleBytes(role.getByteString()).build();
return Util.toCompletableFuture(this.stub.userRevokeRole(userRevokeRoleRequest), AuthUserRevokeRoleResponse::new,
this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<AuthRoleAddResponse> roleAdd(ByteSequence user) {
requireNonNull(user, "user can't be null");
checkNotNull(user, "user can't be null");
AuthRoleAddRequest roleAddRequest = AuthRoleAddRequest.newBuilder().setNameBytes(ByteString.copyFrom(user.getBytes()))
.build();
return completable(
this.stub.roleAdd(roleAddRequest),
AuthRoleAddResponse::new);
AuthRoleAddRequest roleAddRequest = AuthRoleAddRequest.newBuilder().setNameBytes(user.getByteString()).build();
return Util.toCompletableFuture(this.stub.roleAdd(roleAddRequest), AuthRoleAddResponse::new,
this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<AuthRoleGrantPermissionResponse> roleGrantPermission(ByteSequence role, ByteSequence key,
ByteSequence rangeEnd, Permission.Type permType) {
requireNonNull(role, "role can't be null");
requireNonNull(key, "key can't be null");
requireNonNull(rangeEnd, "rangeEnd can't be null");
requireNonNull(permType, "permType can't be null");
checkNotNull(role, "role can't be null");
checkNotNull(key, "key can't be null");
checkNotNull(rangeEnd, "rangeEnd can't be null");
checkNotNull(permType, "permType can't be null");
io.etcd.jetcd.api.Permission.Type type;
switch (permType) {
@ -218,70 +183,54 @@ final class AuthImpl extends Impl implements Auth {
}
io.etcd.jetcd.api.Permission perm = io.etcd.jetcd.api.Permission.newBuilder()
.setKey(ByteString.copyFrom(key.getBytes()))
.setRangeEnd(ByteString.copyFrom(rangeEnd.getBytes()))
.setKey(key.getByteString())
.setRangeEnd(rangeEnd.getByteString())
.setPermType(type)
.build();
AuthRoleGrantPermissionRequest roleGrantPermissionRequest = AuthRoleGrantPermissionRequest.newBuilder()
.setNameBytes(ByteString.copyFrom(role.getBytes()))
.setNameBytes(role.getByteString())
.setPerm(perm)
.build();
return completable(
this.stub.roleGrantPermission(roleGrantPermissionRequest),
AuthRoleGrantPermissionResponse::new);
return Util.toCompletableFuture(this.stub.roleGrantPermission(roleGrantPermissionRequest),
AuthRoleGrantPermissionResponse::new, this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<AuthRoleGetResponse> roleGet(ByteSequence role) {
requireNonNull(role, "role can't be null");
checkNotNull(role, "role can't be null");
AuthRoleGetRequest roleGetRequest = AuthRoleGetRequest.newBuilder()
.setRoleBytes(ByteString.copyFrom(role.getBytes()))
.build();
return completable(
this.stub.roleGet(roleGetRequest),
AuthRoleGetResponse::new);
AuthRoleGetRequest roleGetRequest = AuthRoleGetRequest.newBuilder().setRoleBytes(role.getByteString()).build();
return Util.toCompletableFuture(this.stub.roleGet(roleGetRequest), AuthRoleGetResponse::new,
this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<AuthRoleListResponse> roleList() {
AuthRoleListRequest roleListRequest = AuthRoleListRequest.getDefaultInstance();
return completable(
this.stub.roleList(roleListRequest),
AuthRoleListResponse::new);
return Util.toCompletableFuture(this.stub.roleList(roleListRequest), AuthRoleListResponse::new,
this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<AuthRoleRevokePermissionResponse> roleRevokePermission(ByteSequence role, ByteSequence key,
ByteSequence rangeEnd) {
requireNonNull(role, "role can't be null");
requireNonNull(key, "key can't be null");
requireNonNull(rangeEnd, "rangeEnd can't be null");
checkNotNull(role, "role can't be null");
checkNotNull(key, "key can't be null");
checkNotNull(rangeEnd, "rangeEnd can't be null");
AuthRoleRevokePermissionRequest roleRevokePermissionRequest = AuthRoleRevokePermissionRequest.newBuilder()
.setRoleBytes(ByteString.copyFrom(role.getBytes()))
.setKey(ByteString.copyFrom(key.getBytes()))
.setRangeEnd(ByteString.copyFrom(rangeEnd.getBytes()))
.setRoleBytes(role.getByteString()).setKeyBytes(key.getByteString()).setRangeEndBytes(rangeEnd.getByteString())
.build();
return completable(
this.stub.roleRevokePermission(roleRevokePermissionRequest),
AuthRoleRevokePermissionResponse::new);
return Util.toCompletableFuture(this.stub.roleRevokePermission(roleRevokePermissionRequest),
AuthRoleRevokePermissionResponse::new, this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<AuthRoleDeleteResponse> roleDelete(ByteSequence role) {
requireNonNull(role, "role can't be null");
AuthRoleDeleteRequest roleDeleteRequest = AuthRoleDeleteRequest.newBuilder()
.setRoleBytes(ByteString.copyFrom(role.getBytes()))
.build();
return completable(
this.stub.roleDelete(roleDeleteRequest),
AuthRoleDeleteResponse::new);
checkNotNull(role, "role can't be null");
AuthRoleDeleteRequest roleDeleteRequest = AuthRoleDeleteRequest.newBuilder().setRoleBytes(role.getByteString()).build();
return Util.toCompletableFuture(this.stub.roleDelete(roleDeleteRequest), AuthRoleDeleteResponse::new,
this.connectionManager.getExecutorService());
}
}

View File

@ -17,10 +17,11 @@
package io.etcd.jetcd;
import java.nio.charset.Charset;
import java.util.Objects;
import com.google.protobuf.ByteString;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Etcd binary bytes, easy to convert between byte[], String and ByteString.
*/
@ -32,7 +33,7 @@ public final class ByteSequence {
private final ByteString byteString;
private ByteSequence(ByteString byteString) {
Objects.requireNonNull(byteString, "byteString should not be null");
checkNotNull(byteString, "byteString should not be null");
this.byteString = byteString;
this.hashVal = byteString.hashCode();
}
@ -48,7 +49,9 @@ public final class ByteSequence {
if (prefix == null) {
return false;
}
return byteString.startsWith(prefix.byteString);
ByteString baseByteString = this.getByteString();
ByteString prefixByteString = prefix.getByteString();
return baseByteString.startsWith(prefixByteString);
}
/**
@ -58,19 +61,8 @@ public final class ByteSequence {
* @return a new {@code ByteSequence} instance
*/
public ByteSequence concat(ByteSequence other) {
Objects.requireNonNull(other, "other byteSequence should not be null");
return new ByteSequence(this.byteString.concat(other.byteString));
}
/**
* Concatenate the given {@code ByteSequence} to this one.
*
* @param other string to concatenate
* @return a new {@code ByteSequence} instance
*/
public ByteSequence concat(ByteString other) {
Objects.requireNonNull(other, "other byteSequence should not be null");
return new ByteSequence(this.byteString.concat(other));
checkNotNull(other, "other byteSequence should not be null");
return new ByteSequence(this.byteString.concat(other.getByteString()));
}
/**
@ -122,6 +114,10 @@ public final class ByteSequence {
return hashVal;
}
ByteString getByteString() {
return this.byteString;
}
public String toString(Charset charset) {
return byteString.toString(charset);
}
@ -155,22 +151,10 @@ public final class ByteSequence {
return new ByteSequence(ByteString.copyFrom(bytes));
}
/**
* Create new ByteSequence from a {@link ByteString}.
*
* @param source input {@link ByteString}
* @return the ByteSequence
*/
public static ByteSequence from(ByteString source) {
return new ByteSequence(source);
}
/**
* Create new ByteSequence from raw bytes.
*
* @param source input bytes
* @return the ByteSequence
*/
public static ByteSequence from(byte[] source) {
return new ByteSequence(ByteString.copyFrom(source));
}

View File

@ -27,57 +27,41 @@ public interface Client extends AutoCloseable {
/**
* Returns the {@link Auth} client.
*
* @return the client.
*/
Auth getAuthClient();
/**
* Returns the {@link KV} client.
*
* @return the client.
*/
KV getKVClient();
/**
* Returns the {@link Cluster} client.
*
* @return the client.
*/
Cluster getClusterClient();
/**
* Returns the {@link Maintenance} client.
*
* @return the client.
*/
Maintenance getMaintenanceClient();
/**
* Returns the {@link Lease} client.
*
* @return the client.
*/
Lease getLeaseClient();
/**
* Returns the {@link Watch} client.
*
* @return the client.
*/
Watch getWatchClient();
/**
* Returns the {@link Lock} client.
*
* @return the client.
*/
Lock getLockClient();
/**
* Returns the {@link Election} client.
*
* @return the client.
*/
Election getElectionClient();
@ -86,8 +70,6 @@ public interface Client extends AutoCloseable {
/**
* Returns a new {@link ClientBuilder}.
*
* @return the builder.
*/
static ClientBuilder builder() {
return new ClientBuilder();

View File

@ -19,30 +19,37 @@ package io.etcd.jetcd;
import java.net.URI;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.net.ssl.SSLException;
import io.etcd.jetcd.common.exception.EtcdException;
import io.etcd.jetcd.common.exception.EtcdExceptionFactory;
import io.etcd.jetcd.impl.ClientImpl;
import io.etcd.jetcd.resolver.IPNameResolver;
import io.grpc.ClientInterceptor;
import io.grpc.Metadata;
import io.grpc.netty.GrpcSslContexts;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.vertx.core.Vertx;
import com.google.common.base.Strings;
import com.google.common.collect.Streams;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
* ClientBuilder knows how to create a Client instance.
* ClientBuilder knows how to create an Client instance.
*/
public final class ClientBuilder implements Cloneable {
@ -61,7 +68,6 @@ public final class ClientBuilder implements Cloneable {
private ByteSequence namespace = ByteSequence.EMPTY;
private long retryDelay = 500;
private long retryMaxDelay = 2500;
private int retryMaxAttempts = 2;
private ChronoUnit retryChronoUnit = ChronoUnit.MILLIS;
private Duration keepaliveTime = Duration.ofSeconds(30L);
private Duration keepaliveTimeout = Duration.ofSeconds(10L);
@ -69,7 +75,6 @@ public final class ClientBuilder implements Cloneable {
private Duration retryMaxDuration;
private Duration connectTimeout;
private boolean waitForReady = true;
private Vertx vertx;
ClientBuilder() {
}
@ -91,7 +96,7 @@ public final class ClientBuilder implements Cloneable {
* @throws NullPointerException if target is null or one of endpoint is null
*/
public ClientBuilder target(String target) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(target), "target can't be null or empty");
checkArgument(!Strings.isNullOrEmpty(target), "target can't be null or empty");
this.target = target;
@ -132,7 +137,7 @@ public final class ClientBuilder implements Cloneable {
* @throws IllegalArgumentException if some endpoint is invalid
*/
public ClientBuilder endpoints(Iterable<URI> endpoints) {
Objects.requireNonNull(endpoints, "endpoints can't be null");
checkNotNull(endpoints, "endpoints can't be null");
endpoints.forEach(e -> {
if (e.getHost() == null) {
@ -140,8 +145,8 @@ public final class ClientBuilder implements Cloneable {
}
});
final String target = StreamSupport.stream(endpoints.spliterator(), false)
.map(e -> e.getHost() + (e.getPort() != -1 ? ":" + e.getPort() : ""))
final String target = Streams.stream(endpoints)
.map(e -> e.getHost() + (e.getPort() != -1 ? (":" + e.getPort()) : ""))
.distinct()
.collect(Collectors.joining(","));
@ -159,8 +164,6 @@ public final class ClientBuilder implements Cloneable {
/**
* Returns the auth user
*
* @return the user.
*/
public ByteSequence user() {
return user;
@ -174,15 +177,13 @@ public final class ClientBuilder implements Cloneable {
* @throws NullPointerException if user is <code>null</code>
*/
public ClientBuilder user(ByteSequence user) {
Objects.requireNonNull(user, "user can't be null");
checkNotNull(user, "user can't be null");
this.user = user;
return this;
}
/**
* Returns the auth password
*
* @return the password.
*/
public ByteSequence password() {
return password;
@ -196,15 +197,13 @@ public final class ClientBuilder implements Cloneable {
* @throws NullPointerException if password is <code>null</code>
*/
public ClientBuilder password(ByteSequence password) {
Objects.requireNonNull(password, "password can't be null");
checkNotNull(password, "password can't be null");
this.password = password;
return this;
}
/**
* Returns the namespace of each key used
*
* @return the namespace.
*/
public ByteSequence namespace() {
return namespace;
@ -219,15 +218,13 @@ public final class ClientBuilder implements Cloneable {
* @throws NullPointerException if namespace is <code>null</code>
*/
public ClientBuilder namespace(ByteSequence namespace) {
Objects.requireNonNull(namespace, "namespace can't be null");
checkNotNull(namespace, "namespace can't be null");
this.namespace = namespace;
return this;
}
/**
* Returns the executor service
*
* @return the executor service.
*/
public ExecutorService executorService() {
return executorService;
@ -241,7 +238,7 @@ public final class ClientBuilder implements Cloneable {
* @throws NullPointerException if executorService is <code>null</code>
*/
public ClientBuilder executorService(ExecutorService executorService) {
Objects.requireNonNull(executorService, "executorService can't be null");
checkNotNull(executorService, "executorService can't be null");
this.executorService = executorService;
return this;
}
@ -254,7 +251,7 @@ public final class ClientBuilder implements Cloneable {
* @throws NullPointerException if loadBalancerPolicy is <code>null</code>
*/
public ClientBuilder loadBalancerPolicy(String loadBalancerPolicy) {
Objects.requireNonNull(loadBalancerPolicy, "loadBalancerPolicy can't be null");
checkNotNull(loadBalancerPolicy, "loadBalancerPolicy can't be null");
this.loadBalancerPolicy = loadBalancerPolicy;
return this;
}
@ -270,8 +267,6 @@ public final class ClientBuilder implements Cloneable {
/**
* Returns the ssl context
*
* @return the ssl context.
*/
public SslContext sslContext() {
return sslContext;
@ -305,8 +300,6 @@ public final class ClientBuilder implements Cloneable {
/**
* Returns The authority used to authenticate connections to servers.
*
* @return the authority.
*/
public String authority() {
return authority;
@ -325,8 +318,6 @@ public final class ClientBuilder implements Cloneable {
/**
* Returns the maximum message size allowed for a single gRPC frame.
*
* @return max inbound message size.
*/
public Integer maxInboundMessageSize() {
return maxInboundMessageSize;
@ -345,8 +336,6 @@ public final class ClientBuilder implements Cloneable {
/**
* Returns the headers to be added to http request headers
*
* @return headers.
*/
public Map<Metadata.Key<?>, Object> headers() {
return headers == null ? Collections.emptyMap() : Collections.unmodifiableMap(headers);
@ -383,8 +372,6 @@ public final class ClientBuilder implements Cloneable {
/**
* Returns the headers to be added to auth request headers
*
* @return auth headers.
*/
public Map<Metadata.Key<?>, Object> authHeaders() {
return authHeaders == null ? Collections.emptyMap() : Collections.unmodifiableMap(authHeaders);
@ -421,8 +408,6 @@ public final class ClientBuilder implements Cloneable {
/**
* Returns the interceptors
*
* @return the interceptors.
*/
public List<ClientInterceptor> interceptors() {
return interceptors;
@ -460,8 +445,6 @@ public final class ClientBuilder implements Cloneable {
/**
* Returns the auth interceptors
*
* @return the interceptors.
*/
public List<ClientInterceptor> authInterceptors() {
return authInterceptors;
@ -499,8 +482,6 @@ public final class ClientBuilder implements Cloneable {
/**
* Returns The delay between retries.
*
* @return the retry delay.
*/
public long retryDelay() {
return retryDelay;
@ -519,8 +500,6 @@ public final class ClientBuilder implements Cloneable {
/**
* Returns the max backing off delay between retries
*
* @return max retry delay.
*/
public long retryMaxDelay() {
return retryMaxDelay;
@ -537,31 +516,6 @@ public final class ClientBuilder implements Cloneable {
return this;
}
/**
* Returns the max number of retry attempts
*
* @return max retry attempts.
*/
public int retryMaxAttempts() {
return retryMaxAttempts;
}
/**
* Set the max number of retry attempts
*
* @param retryMaxAttempts The max retry attempts.
* @return this builder
*/
public ClientBuilder retryMaxAttempts(int retryMaxAttempts) {
this.retryMaxAttempts = retryMaxAttempts;
return this;
}
/**
* Returns the keep alive time.
*
* @return keep alive time.
*/
public Duration keepaliveTime() {
return keepaliveTime;
}
@ -580,11 +534,6 @@ public final class ClientBuilder implements Cloneable {
return this;
}
/**
* Returns the keep alive time out.
*
* @return keep alive time out.
*/
public Duration keepaliveTimeout() {
return keepaliveTimeout;
}
@ -617,8 +566,6 @@ public final class ClientBuilder implements Cloneable {
/**
* Returns he retries period unit.
*
* @return the chrono unit.
*/
public ChronoUnit retryChronoUnit() {
return retryChronoUnit;
@ -637,8 +584,6 @@ public final class ClientBuilder implements Cloneable {
/**
* Returns the retries max duration.
*
* @return retry max duration.
*/
public Duration retryMaxDuration() {
return retryMaxDuration;
@ -646,8 +591,6 @@ public final class ClientBuilder implements Cloneable {
/**
* Returns the connect timeout.
*
* @return connect timeout.
*/
public Duration connectTimeout() {
return connectTimeout;
@ -709,30 +652,6 @@ public final class ClientBuilder implements Cloneable {
return this;
}
/**
* Gets the Vertx instance.
*
* @return the vertx instance.
*/
public Vertx vertx() {
return vertx;
}
/**
* configure Vertx instance.
*
* @param vertx Vertx instance to use.
* @return this builder to train
* @throws IllegalArgumentException if vertx is null
*/
public ClientBuilder vertx(Vertx vertx) {
Preconditions.checkArgument(vertx != null, "vertx can't be null");
this.vertx = vertx;
return this;
}
/**
* build a new Client.
*
@ -740,15 +659,13 @@ public final class ClientBuilder implements Cloneable {
* @throws EtcdException if client experiences build error.
*/
public Client build() {
Preconditions.checkState(target != null, "please configure etcd server endpoints before build.");
checkState(target != null, "please configure etcd server endpoints before build.");
return new ClientImpl(this);
}
/**
* Returns a copy of this builder
*
* @return a copy of the builder.
*/
public ClientBuilder copy() {
try {

View File

@ -14,35 +14,50 @@
* limitations under the License.
*/
package io.etcd.jetcd.impl;
package io.etcd.jetcd;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.ClientBuilder;
import io.etcd.jetcd.support.Util;
import io.grpc.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.etcd.jetcd.auth.AuthInterceptor;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.netty.NegotiationType;
import io.grpc.netty.NettyChannelBuilder;
import io.grpc.stub.AbstractStub;
import io.netty.channel.ChannelOption;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.grpc.VertxChannelBuilder;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ListenableFuture;
import net.jodah.failsafe.Failsafe;
import net.jodah.failsafe.RetryPolicy;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.toEtcdException;
final class ClientConnectionManager {
private static final Logger LOGGER = LoggerFactory.getLogger(ClientConnectionManager.class);
private final Object lock;
private final ClientBuilder builder;
private final ExecutorService executorService;
private final AuthCredential credential;
private volatile Vertx vertx;
private final AuthInterceptor authInterceptor;
private volatile ManagedChannel managedChannel;
ClientConnectionManager(ClientBuilder builder) {
@ -52,19 +67,14 @@ final class ClientConnectionManager {
ClientConnectionManager(ClientBuilder builder, ManagedChannel managedChannel) {
this.lock = new Object();
this.builder = builder;
this.authInterceptor = new AuthInterceptor(builder);
this.managedChannel = managedChannel;
this.credential = new AuthCredential(this);
if (builder.executorService() == null) {
// default to daemon
this.executorService = Executors.newCachedThreadPool(Util.createThreadFactory("jetcd-", true));
this.executorService = Executors.newCachedThreadPool();
} else {
this.executorService = builder.executorService();
}
if (builder.vertx() != null) {
this.vertx = builder.vertx();
}
}
ManagedChannel getChannel() {
@ -87,12 +97,8 @@ final class ClientConnectionManager {
return executorService;
}
ClientBuilder builder() {
return builder;
}
AuthCredential authCredential() {
return this.credential;
AuthInterceptor authInterceptor() {
return authInterceptor;
}
/**
@ -107,15 +113,12 @@ final class ClientConnectionManager {
}
private <T extends AbstractStub<T>> T newStub(Function<ManagedChannel, T> stubCustomizer, ManagedChannel channel) {
T stub = stubCustomizer.apply(channel);
final T stub = stubCustomizer.apply(channel);
if (builder.waitForReady()) {
stub = stub.withWaitForReady();
return stub.withWaitForReady();
} else {
return stub;
}
if (builder.user() != null && builder.password() != null) {
stub = stub.withCallCredentials(this.authCredential());
}
return stub;
}
void close() {
@ -123,9 +126,6 @@ final class ClientConnectionManager {
if (managedChannel != null) {
managedChannel.shutdownNow();
}
if (vertx != null) {
vertx.close();
}
}
if (builder.executorService() == null) {
@ -133,8 +133,7 @@ final class ClientConnectionManager {
}
}
<T extends AbstractStub<T>, R> CompletableFuture<R> withNewChannel(
String target,
<T extends AbstractStub<T>, R> CompletableFuture<R> withNewChannel(String target,
Function<ManagedChannel, T> stubCustomizer,
Function<T, CompletableFuture<R>> stubConsumer) {
@ -149,17 +148,19 @@ final class ClientConnectionManager {
}
}
@VisibleForTesting
ManagedChannelBuilder<?> defaultChannelBuilder() {
return defaultChannelBuilder(builder.target());
}
@VisibleForTesting
@SuppressWarnings("rawtypes")
ManagedChannelBuilder<?> defaultChannelBuilder(String target) {
if (target == null) {
throw new IllegalArgumentException("At least one endpoint should be provided");
}
final VertxChannelBuilder channelBuilder = VertxChannelBuilder.forTarget(vertx(), target);
final NettyChannelBuilder channelBuilder = NettyChannelBuilder.forTarget(target);
if (builder.authority() != null) {
channelBuilder.overrideAuthority(builder.authority());
@ -168,10 +169,10 @@ final class ClientConnectionManager {
channelBuilder.maxInboundMessageSize(builder.maxInboundMessageSize());
}
if (builder.sslContext() != null) {
channelBuilder.nettyBuilder().negotiationType(NegotiationType.TLS);
channelBuilder.nettyBuilder().sslContext(builder.sslContext());
channelBuilder.negotiationType(NegotiationType.TLS);
channelBuilder.sslContext(builder.sslContext());
} else {
channelBuilder.nettyBuilder().negotiationType(NegotiationType.PLAINTEXT);
channelBuilder.negotiationType(NegotiationType.PLAINTEXT);
}
if (builder.keepaliveTime() != null) {
@ -184,24 +185,22 @@ final class ClientConnectionManager {
channelBuilder.keepAliveWithoutCalls(builder.keepaliveWithoutCalls());
}
if (builder.connectTimeout() != null) {
channelBuilder.nettyBuilder().withOption(ChannelOption.CONNECT_TIMEOUT_MILLIS,
(int) builder.connectTimeout().toMillis());
channelBuilder.withOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) builder.connectTimeout().toMillis());
}
if (builder.loadBalancerPolicy() != null) {
channelBuilder.defaultLoadBalancingPolicy(builder.loadBalancerPolicy());
} else {
channelBuilder.defaultLoadBalancingPolicy("round_robin");
channelBuilder.defaultLoadBalancingPolicy("pick_first");
}
channelBuilder.intercept(authInterceptor);
if (builder.headers() != null) {
channelBuilder.intercept(new ClientInterceptor() {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method,
CallOptions callOptions,
Channel next) {
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method,
CallOptions callOptions, Channel next) {
return new SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
@ -220,15 +219,59 @@ final class ClientConnectionManager {
return channelBuilder;
}
Vertx vertx() {
if (this.vertx == null) {
synchronized (this.lock) {
if (this.vertx == null) {
this.vertx = Vertx.vertx(new VertxOptions().setUseDaemonThread(true));
}
}
/**
* execute the task and retry it in case of failure.
*
* @param task a function that returns a new ListenableFuture.
* @param resultConvert a function that converts Type S to Type T.
* @param <S> Source type
* @param <T> Converted Type.
* @return a CompletableFuture with type T.
*/
public <S, T> CompletableFuture<T> execute(Callable<ListenableFuture<S>> task, Function<S, T> resultConvert) {
return execute(task, resultConvert, Util::isRetryable);
}
/**
* execute the task and retry it in case of failure.
*
* @param task a function that returns a new SourceFuture.
* @param resultConvert a function that converts Type S to Type T.
* @param doRetry a function that determines the retry condition base on SourceFuture error.
* @param <S> Source type
* @param <T> Converted Type.
* @return a CompletableFuture with type T.
*/
@SuppressWarnings("FutureReturnValueIgnored")
public <S, T> CompletableFuture<T> execute(
Callable<ListenableFuture<S>> task,
Function<S, T> resultConvert,
Predicate<Throwable> doRetry) {
RetryPolicy<CompletableFuture<S>> retryPolicy = new RetryPolicy<CompletableFuture<S>>().handleIf(doRetry)
.onRetriesExceeded(e -> LOGGER.warn("maximum number of auto retries reached"))
.withBackoff(builder.retryDelay(), builder.retryMaxDelay(), builder.retryChronoUnit());
if (builder.retryMaxDuration() != null) {
retryPolicy = retryPolicy.withMaxDuration(builder.retryMaxDuration());
}
return this.vertx;
return Failsafe.with(retryPolicy).with(executorService)
.getAsyncExecution(execution -> {
CompletableFuture<S> wrappedFuture = new CompletableFuture<>();
ListenableFuture<S> future = task.call();
future.addListener(() -> {
try {
wrappedFuture.complete(future.get());
execution.complete(wrappedFuture);
} catch (Exception error) {
if (!execution.retryOn(error)) {
// permanent failure
wrappedFuture.completeExceptionally(error);
}
}
}, executorService);
}).thenCompose(f -> f.thenApply(resultConvert));
}
}

View File

@ -14,45 +14,35 @@
* limitations under the License.
*/
package io.etcd.jetcd.impl;
package io.etcd.jetcd;
import io.etcd.jetcd.Auth;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.ClientBuilder;
import io.etcd.jetcd.Cluster;
import io.etcd.jetcd.Election;
import io.etcd.jetcd.KV;
import io.etcd.jetcd.Lease;
import io.etcd.jetcd.Lock;
import io.etcd.jetcd.Maintenance;
import io.etcd.jetcd.Watch;
import io.etcd.jetcd.support.MemorizingClientSupplier;
import io.etcd.jetcd.support.MemoizingClientSupplier;
/**
* Etcd Client.
*/
public final class ClientImpl implements Client {
final class ClientImpl implements Client {
private final ClientConnectionManager connectionManager;
private final MemorizingClientSupplier<KV> kvClient;
private final MemorizingClientSupplier<Auth> authClient;
private final MemorizingClientSupplier<Maintenance> maintenanceClient;
private final MemorizingClientSupplier<Cluster> clusterClient;
private final MemorizingClientSupplier<Lease> leaseClient;
private final MemorizingClientSupplier<Watch> watchClient;
private final MemorizingClientSupplier<Lock> lockClient;
private final MemorizingClientSupplier<Election> electionClient;
private final MemoizingClientSupplier<KV> kvClient;
private final MemoizingClientSupplier<Auth> authClient;
private final MemoizingClientSupplier<Maintenance> maintenanceClient;
private final MemoizingClientSupplier<Cluster> clusterClient;
private final MemoizingClientSupplier<Lease> leaseClient;
private final MemoizingClientSupplier<Watch> watchClient;
private final MemoizingClientSupplier<Lock> lockClient;
private final MemoizingClientSupplier<Election> electionClient;
public ClientImpl(ClientBuilder clientBuilder) {
this.connectionManager = new ClientConnectionManager(clientBuilder.copy());
this.kvClient = new MemorizingClientSupplier<>(() -> new KVImpl(this.connectionManager));
this.authClient = new MemorizingClientSupplier<>(() -> new AuthImpl(this.connectionManager));
this.maintenanceClient = new MemorizingClientSupplier<>(() -> new MaintenanceImpl(this.connectionManager));
this.clusterClient = new MemorizingClientSupplier<>(() -> new ClusterImpl(this.connectionManager));
this.leaseClient = new MemorizingClientSupplier<>(() -> new LeaseImpl(this.connectionManager));
this.watchClient = new MemorizingClientSupplier<>(() -> new WatchImpl(this.connectionManager));
this.lockClient = new MemorizingClientSupplier<>(() -> new LockImpl(this.connectionManager));
this.electionClient = new MemorizingClientSupplier<>(() -> new ElectionImpl(this.connectionManager));
this.kvClient = new MemoizingClientSupplier<>(() -> new KVImpl(this.connectionManager));
this.authClient = new MemoizingClientSupplier<>(() -> new AuthImpl(this.connectionManager));
this.maintenanceClient = new MemoizingClientSupplier<>(() -> new MaintenanceImpl(this.connectionManager));
this.clusterClient = new MemoizingClientSupplier<>(() -> new ClusterImpl(this.connectionManager));
this.leaseClient = new MemoizingClientSupplier<>(() -> new LeaseImpl(this.connectionManager));
this.watchClient = new MemoizingClientSupplier<>(() -> new WatchImpl(this.connectionManager));
this.lockClient = new MemoizingClientSupplier<>(() -> new LockImpl(this.connectionManager));
this.electionClient = new MemoizingClientSupplier<>(() -> new ElectionImpl(this.connectionManager));
}
@Override

View File

@ -39,22 +39,13 @@ public interface Cluster extends CloseableClient {
CompletableFuture<MemberListResponse> listMember();
/**
* add a non-learner new member into the cluster.
* add a new member into the cluster.
*
* @param peerAddrs the peer addresses of the new member
* @return the response
*/
CompletableFuture<MemberAddResponse> addMember(List<URI> peerAddrs);
/**
* add a new member into the cluster.
*
* @param peerAddrs the peer addresses of the new member
* @param isLearner whether the member is raft learner
* @return the response
*/
CompletableFuture<MemberAddResponse> addMember(List<URI> peerAddrs, boolean isLearner);
/**
* removes an existing member from the cluster.
*

View File

@ -14,19 +14,18 @@
* limitations under the License.
*/
package io.etcd.jetcd.impl;
package io.etcd.jetcd;
import java.net.URI;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import io.etcd.jetcd.Cluster;
import io.etcd.jetcd.api.ClusterGrpc;
import io.etcd.jetcd.api.MemberAddRequest;
import io.etcd.jetcd.api.MemberListRequest;
import io.etcd.jetcd.api.MemberRemoveRequest;
import io.etcd.jetcd.api.MemberUpdateRequest;
import io.etcd.jetcd.api.VertxClusterGrpc;
import io.etcd.jetcd.cluster.MemberAddResponse;
import io.etcd.jetcd.cluster.MemberListResponse;
import io.etcd.jetcd.cluster.MemberRemoveResponse;
@ -35,14 +34,14 @@ import io.etcd.jetcd.cluster.MemberUpdateResponse;
/**
* Implementation of cluster client.
*/
final class ClusterImpl extends Impl implements Cluster {
final class ClusterImpl implements Cluster {
private final VertxClusterGrpc.ClusterVertxStub stub;
private final ClusterGrpc.ClusterFutureStub stub;
private final ClientConnectionManager connectionManager;
ClusterImpl(ClientConnectionManager connectionManager) {
super(connectionManager);
this.stub = connectionManager.newStub(VertxClusterGrpc::newVertxStub);
this.connectionManager = connectionManager;
this.stub = connectionManager.newStub(ClusterGrpc::newFutureStub);
}
/**
@ -50,37 +49,27 @@ final class ClusterImpl extends Impl implements Cluster {
*/
@Override
public CompletableFuture<MemberListResponse> listMember() {
return completable(
return Util.toCompletableFuture(
this.stub.memberList(MemberListRequest.getDefaultInstance()),
MemberListResponse::new);
}
/**
* add a non-learner new member into the cluster.
*
* @param peerAddrs the peer addresses of the new member
*/
@Override
public CompletableFuture<MemberAddResponse> addMember(List<URI> peerAddrs) {
return addMember(peerAddrs, false);
MemberListResponse::new,
this.connectionManager.getExecutorService());
}
/**
* add a new member into the cluster.
*
* @param peerAddrs the peer addresses of the new member
* @param isLearner whether the member is raft learner
*/
@Override
public CompletableFuture<MemberAddResponse> addMember(List<URI> peerAddrs, boolean isLearner) {
public CompletableFuture<MemberAddResponse> addMember(List<URI> peerAddrs) {
MemberAddRequest memberAddRequest = MemberAddRequest.newBuilder()
.addAllPeerURLs(peerAddrs.stream().map(URI::toString).collect(Collectors.toList()))
.setIsLearner(isLearner)
.build();
return completable(
return Util.toCompletableFuture(
this.stub.memberAdd(memberAddRequest),
MemberAddResponse::new);
MemberAddResponse::new,
this.connectionManager.getExecutorService());
}
/**
@ -94,9 +83,10 @@ final class ClusterImpl extends Impl implements Cluster {
.setID(memberID)
.build();
return completable(
return Util.toCompletableFuture(
this.stub.memberRemove(memberRemoveRequest),
MemberRemoveResponse::new);
MemberRemoveResponse::new,
this.connectionManager.getExecutorService());
}
/**
@ -108,12 +98,12 @@ final class ClusterImpl extends Impl implements Cluster {
@Override
public CompletableFuture<MemberUpdateResponse> updateMember(long memberID, List<URI> peerAddrs) {
MemberUpdateRequest memberUpdateRequest = MemberUpdateRequest.newBuilder()
.addAllPeerURLs(peerAddrs.stream().map(URI::toString).collect(Collectors.toList()))
.setID(memberID)
.addAllPeerURLs(peerAddrs.stream().map(URI::toString).collect(Collectors.toList())).setID(memberID)
.build();
return completable(
return Util.toCompletableFuture(
this.stub.memberUpdate(memberUpdateRequest),
MemberUpdateResponse::new);
MemberUpdateResponse::new,
this.connectionManager.getExecutorService());
}
}

View File

@ -22,11 +22,11 @@ import io.grpc.Metadata;
* Constants of Etcd.
*/
public class Constants {
public static final String TOKEN = "token";
public static final ByteSequence NULL_KEY = ByteSequence.from(new byte[] { '\0' });
public static final Metadata.Key<String> REQUIRE_LEADER_KEY = Metadata.Key.of(
"hasleader",
public static final Metadata.Key<String> REQUIRE_LEADER_KEY = Metadata.Key.of("hasleader",
Metadata.ASCII_STRING_MARSHALLER);
public static final String REQUIRE_LEADER_VALUE = "true";
}

View File

@ -0,0 +1,185 @@
/*
* Copyright 2016-2021 The jetcd 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.
*/
package io.etcd.jetcd;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import io.etcd.jetcd.api.CampaignRequest;
import io.etcd.jetcd.api.ElectionGrpc;
import io.etcd.jetcd.api.LeaderRequest;
import io.etcd.jetcd.api.ProclaimRequest;
import io.etcd.jetcd.api.ResignRequest;
import io.etcd.jetcd.common.exception.EtcdExceptionFactory;
import io.etcd.jetcd.election.CampaignResponse;
import io.etcd.jetcd.election.LeaderKey;
import io.etcd.jetcd.election.LeaderResponse;
import io.etcd.jetcd.election.NoLeaderException;
import io.etcd.jetcd.election.NotLeaderException;
import io.etcd.jetcd.election.ProclaimResponse;
import io.etcd.jetcd.election.ResignResponse;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import static com.google.common.base.Preconditions.checkNotNull;
final class ElectionImpl implements Election {
private final ElectionGrpc.ElectionStub stub;
private final ByteSequence namespace;
ElectionImpl(ClientConnectionManager connectionManager) {
this.stub = connectionManager.newStub(ElectionGrpc::newStub);
this.namespace = connectionManager.getNamespace();
}
@Override
public CompletableFuture<CampaignResponse> campaign(ByteSequence electionName, long leaseId, ByteSequence proposal) {
checkNotNull(electionName, "election name should not be null");
checkNotNull(proposal, "proposal should not be null");
CampaignRequest request = CampaignRequest.newBuilder()
.setName(Util.prefixNamespace(electionName.getByteString(), namespace))
.setValue(proposal.getByteString())
.setLease(leaseId)
.build();
final StreamObserverDelegate<io.etcd.jetcd.api.CampaignResponse, CampaignResponse> observer = new StreamObserverDelegate<>(
CampaignResponse::new);
stub.campaign(request, observer);
return observer.getFuture();
}
@Override
public CompletableFuture<ProclaimResponse> proclaim(LeaderKey leaderKey, ByteSequence proposal) {
checkNotNull(leaderKey, "leader key should not be null");
checkNotNull(proposal, "proposal should not be null");
io.etcd.jetcd.api.LeaderKey leader = io.etcd.jetcd.api.LeaderKey.newBuilder()
.setKey(leaderKey.getKey()).setName(leaderKey.getName())
.setLease(leaderKey.getLease()).setRev(leaderKey.getRevision())
.build();
ProclaimRequest request = ProclaimRequest.newBuilder()
.setLeader(leader).setValue(proposal.getByteString())
.build();
final StreamObserverDelegate<io.etcd.jetcd.api.ProclaimResponse, ProclaimResponse> observer = new StreamObserverDelegate<>(
ProclaimResponse::new);
stub.proclaim(request, observer);
return observer.getFuture();
}
@Override
public CompletableFuture<LeaderResponse> leader(ByteSequence electionName) {
checkNotNull(electionName, "election name should not be null");
LeaderRequest request = LeaderRequest.newBuilder()
.setName(Util.prefixNamespace(electionName.getByteString(), namespace))
.build();
final StreamObserverDelegate<io.etcd.jetcd.api.LeaderResponse, LeaderResponse> observer = new StreamObserverDelegate<>(
input -> new LeaderResponse(input, namespace));
stub.leader(request, observer);
return observer.getFuture();
}
@Override
public void observe(ByteSequence electionName, Listener listener) {
checkNotNull(electionName, "election name should not be null");
checkNotNull(listener, "listener should not be null");
LeaderRequest request = LeaderRequest.newBuilder().setName(electionName.getByteString()).build();
stub.observe(request, new StreamObserver<io.etcd.jetcd.api.LeaderResponse>() {
@Override
public void onNext(io.etcd.jetcd.api.LeaderResponse value) {
listener.onNext(new LeaderResponse(value, namespace));
}
@Override
public void onError(Throwable error) {
listener.onError(EtcdExceptionFactory.toEtcdException(error));
}
@Override
public void onCompleted() {
listener.onCompleted();
}
});
}
@Override
public CompletableFuture<ResignResponse> resign(LeaderKey leaderKey) {
checkNotNull(leaderKey, "leader key should not be null");
io.etcd.jetcd.api.LeaderKey leader = io.etcd.jetcd.api.LeaderKey.newBuilder()
.setKey(leaderKey.getKey()).setName(leaderKey.getName())
.setLease(leaderKey.getLease()).setRev(leaderKey.getRevision())
.build();
ResignRequest request = ResignRequest.newBuilder().setLeader(leader).build();
final StreamObserverDelegate<io.etcd.jetcd.api.ResignResponse, ResignResponse> observer = new StreamObserverDelegate<>(
ResignResponse::new);
stub.resign(request, observer);
return observer.getFuture();
}
@Override
public void close() {
}
private static class StreamObserverDelegate<S, T> implements StreamObserver<S> {
private final CompletableFuture<T> future = new CompletableFuture<>();
private final Function<S, T> converter;
public StreamObserverDelegate(Function<S, T> converter) {
this.converter = converter;
}
@Override
public void onNext(S value) {
future.complete(converter.apply(value));
}
@Override
public void onError(Throwable error) {
if (error instanceof StatusRuntimeException) {
StatusRuntimeException exception = (StatusRuntimeException) error;
String description = exception.getStatus().getDescription();
// different APIs use different messages. we cannot distinguish missing leader error otherwise,
// because communicated status is always UNKNOWN
if ("election: not leader".equals(description)) {
future.completeExceptionally(NotLeaderException.INSTANCE);
} else if ("election: no leader".equals(description)) {
future.completeExceptionally(NoLeaderException.INSTANCE);
}
}
future.completeExceptionally(EtcdExceptionFactory.toEtcdException(error));
}
@Override
public void onCompleted() {
}
public CompletableFuture<T> getFuture() {
return future;
}
}
}

View File

@ -26,7 +26,6 @@ import io.etcd.jetcd.options.CompactOption;
import io.etcd.jetcd.options.DeleteOption;
import io.etcd.jetcd.options.GetOption;
import io.etcd.jetcd.options.PutOption;
import io.etcd.jetcd.options.TxnOption;
import io.etcd.jetcd.support.CloseableClient;
/**
@ -116,12 +115,4 @@ public interface KV extends CloseableClient {
* @return a Txn
*/
Txn txn();
/**
* creates a transaction.
*
* @param option TxnOption
* @return a Txn
*/
Txn txn(TxnOption option);
}

View File

@ -0,0 +1,181 @@
/*
* Copyright 2016-2021 The jetcd 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.
*/
package io.etcd.jetcd;
import java.util.concurrent.CompletableFuture;
import io.etcd.jetcd.api.CompactionRequest;
import io.etcd.jetcd.api.DeleteRangeRequest;
import io.etcd.jetcd.api.KVGrpc;
import io.etcd.jetcd.api.PutRequest;
import io.etcd.jetcd.api.RangeRequest;
import io.etcd.jetcd.kv.CompactResponse;
import io.etcd.jetcd.kv.DeleteResponse;
import io.etcd.jetcd.kv.GetResponse;
import io.etcd.jetcd.kv.PutResponse;
import io.etcd.jetcd.kv.TxnResponse;
import io.etcd.jetcd.op.TxnImpl;
import io.etcd.jetcd.options.CompactOption;
import io.etcd.jetcd.options.DeleteOption;
import io.etcd.jetcd.options.GetOption;
import io.etcd.jetcd.options.OptionsUtil;
import io.etcd.jetcd.options.PutOption;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.etcd.jetcd.options.OptionsUtil.toRangeRequestSortOrder;
import static io.etcd.jetcd.options.OptionsUtil.toRangeRequestSortTarget;
/**
* Implementation of etcd kv client.
*/
final class KVImpl implements KV {
private final ClientConnectionManager connectionManager;
private final KVGrpc.KVFutureStub stub;
private final ByteSequence namespace;
KVImpl(ClientConnectionManager connectionManager) {
this.connectionManager = connectionManager;
this.stub = connectionManager.newStub(KVGrpc::newFutureStub);
this.namespace = connectionManager.getNamespace();
}
@Override
public CompletableFuture<PutResponse> put(ByteSequence key, ByteSequence value) {
return this.put(key, value, PutOption.DEFAULT);
}
@Override
public CompletableFuture<PutResponse> put(ByteSequence key, ByteSequence value, PutOption option) {
checkNotNull(key, "key should not be null");
checkNotNull(value, "value should not be null");
checkNotNull(option, "option should not be null");
PutRequest request = PutRequest.newBuilder()
.setKey(Util.prefixNamespace(key.getByteString(), namespace))
.setValue(value.getByteString())
.setLease(option.getLeaseId())
.setPrevKv(option.getPrevKV())
.build();
return connectionManager.execute(
() -> stub.put(request),
response -> new PutResponse(response, namespace),
Util::isRetryable);
}
@Override
public CompletableFuture<GetResponse> get(ByteSequence key) {
return this.get(key, GetOption.DEFAULT);
}
@Override
public CompletableFuture<GetResponse> get(ByteSequence key, GetOption option) {
checkNotNull(key, "key should not be null");
checkNotNull(option, "option should not be null");
RangeRequest.Builder builder = RangeRequest.newBuilder()
.setKey(Util.prefixNamespace(key.getByteString(), namespace))
.setCountOnly(option.isCountOnly())
.setLimit(option.getLimit())
.setRevision(option.getRevision())
.setKeysOnly(option.isKeysOnly())
.setSerializable(option.isSerializable())
.setSortOrder(toRangeRequestSortOrder(option.getSortOrder()))
.setSortTarget(toRangeRequestSortTarget(option.getSortField()))
.setMinCreateRevision(option.getMinCreateRevision())
.setMaxCreateRevision(option.getMaxCreateRevision())
.setMinModRevision(option.getMinModRevision())
.setMaxModRevision(option.getMaxModRevision());
option.getEndKey().map(endKey -> Util.prefixNamespaceToRangeEnd(endKey.getByteString(), namespace))
.ifPresent(builder::setRangeEnd);
if (!option.getEndKey().isPresent() && option.isPrefix()) {
ByteSequence endKey = OptionsUtil.prefixEndOf(key);
builder.setRangeEnd(Util.prefixNamespaceToRangeEnd(endKey.getByteString(), namespace));
}
RangeRequest request = builder.build();
return connectionManager.execute(
() -> stub.range(request),
response -> new GetResponse(response, namespace),
Util::isRetryable);
}
@Override
public CompletableFuture<DeleteResponse> delete(ByteSequence key) {
return this.delete(key, DeleteOption.DEFAULT);
}
@Override
public CompletableFuture<DeleteResponse> delete(ByteSequence key, DeleteOption option) {
checkNotNull(key, "key should not be null");
checkNotNull(option, "option should not be null");
DeleteRangeRequest.Builder builder = DeleteRangeRequest.newBuilder()
.setKey(Util.prefixNamespace(key.getByteString(), namespace))
.setPrevKv(option.isPrevKV());
option.getEndKey()
.map(endKey -> Util.prefixNamespaceToRangeEnd(endKey.getByteString(), namespace))
.ifPresent(builder::setRangeEnd);
if (!option.getEndKey().isPresent() && option.isPrefix()) {
ByteSequence endKey = OptionsUtil.prefixEndOf(key);
builder.setRangeEnd(Util.prefixNamespaceToRangeEnd(endKey.getByteString(), namespace));
}
DeleteRangeRequest request = builder.build();
return connectionManager.execute(
() -> stub.deleteRange(request),
response -> new DeleteResponse(response, namespace),
Util::isRetryable);
}
@Override
public CompletableFuture<CompactResponse> compact(long rev) {
return this.compact(rev, CompactOption.DEFAULT);
}
@Override
public CompletableFuture<CompactResponse> compact(long rev, CompactOption option) {
checkNotNull(option, "option should not be null");
CompactionRequest request = CompactionRequest.newBuilder()
.setRevision(rev).setPhysical(option.isPhysical())
.build();
return connectionManager.execute(
() -> stub.compact(request),
CompactResponse::new,
Util::isRetryable);
}
@Override
public Txn txn() {
return TxnImpl.newTxn(
request -> connectionManager.execute(
() -> stub.txn(request),
response -> new TxnResponse(response, namespace), Util::isRetryable),
namespace);
}
}

View File

@ -16,8 +16,6 @@
package io.etcd.jetcd;
import io.etcd.jetcd.support.Util;
/**
* Etcd key value pair.
*/
@ -29,18 +27,13 @@ public class KeyValue {
public KeyValue(io.etcd.jetcd.api.KeyValue kv, ByteSequence namespace) {
this.kv = kv;
this.unprefixedKey = ByteSequence
.from(kv.getKey().isEmpty() ? kv.getKey() : Util.unprefixNamespace(kv.getKey(), namespace));
this.value = ByteSequence.from(kv.getValue());
this.unprefixedKey = ByteSequence.from(
kv.getKey().isEmpty()
? kv.getKey()
: Util.unprefixNamespace(kv.getKey(), namespace));
}
/**
* Returns the key
*
* @return the key.
*/
public ByteSequence getKey() {
return unprefixedKey;
@ -48,8 +41,6 @@ public class KeyValue {
/**
* Returns the value
*
* @return the value.
*/
public ByteSequence getValue() {
return value;
@ -57,8 +48,6 @@ public class KeyValue {
/**
* Returns the create revision.
*
* @return the create revision.
*/
public long getCreateRevision() {
return kv.getCreateRevision();
@ -66,8 +55,6 @@ public class KeyValue {
/**
* Returns the mod revision.
*
* @return the mod revision.
*/
public long getModRevision() {
return kv.getModRevision();
@ -75,8 +62,6 @@ public class KeyValue {
/**
* Returns the version.
*
* @return the version.
*/
public long getVersion() {
return kv.getVersion();
@ -84,8 +69,6 @@ public class KeyValue {
/**
* Returns the lease.
*
* @return the lease.
*/
public long getLease() {
return kv.getLease();

View File

@ -0,0 +1,354 @@
/*
* Copyright 2016-2021 The jetcd 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.
*/
package io.etcd.jetcd;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.etcd.jetcd.api.LeaseGrantRequest;
import io.etcd.jetcd.api.LeaseGrpc;
import io.etcd.jetcd.api.LeaseKeepAliveRequest;
import io.etcd.jetcd.api.LeaseRevokeRequest;
import io.etcd.jetcd.api.LeaseTimeToLiveRequest;
import io.etcd.jetcd.common.exception.ErrorCode;
import io.etcd.jetcd.lease.LeaseGrantResponse;
import io.etcd.jetcd.lease.LeaseKeepAliveResponse;
import io.etcd.jetcd.lease.LeaseRevokeResponse;
import io.etcd.jetcd.lease.LeaseTimeToLiveResponse;
import io.etcd.jetcd.options.LeaseOption;
import io.etcd.jetcd.support.CloseableClient;
import io.etcd.jetcd.support.Observers;
import io.grpc.stub.StreamObserver;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.newClosedLeaseClientException;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.newEtcdException;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.toEtcdException;
/**
* Implementation of lease client.
*/
final class LeaseImpl implements Lease {
private static final Logger LOG = LoggerFactory.getLogger(LeaseImpl.class);
/**
* FIRST_KEEPALIVE_TIMEOUT_MS is the timeout for the first keepalive request
* before the actual TTL is known to the lease client.
*/
private static final int FIRST_KEEPALIVE_TIMEOUT_MS = 5000;
private final ClientConnectionManager connectionManager;
private final LeaseGrpc.LeaseFutureStub stub;
private final LeaseGrpc.LeaseStub leaseStub;
private final Map<Long, KeepAlive> keepAlives;
/**
* Timer schedule to send keep alive request.
*/
private final ListeningScheduledExecutorService scheduledExecutorService;
private ScheduledFuture<?> keepAliveFuture;
private ScheduledFuture<?> deadlineFuture;
/**
* KeepAlive Request Stream, put request into this stream to keep the lease alive.
*/
private StreamObserver<LeaseKeepAliveRequest> keepAliveRequestObserver;
/**
* KeepAlive Response Streamer, receive keep alive response from this stream and update the
* nextKeepAliveTime and deadline of the leases.
*/
private StreamObserver<io.etcd.jetcd.api.LeaseKeepAliveResponse> keepAliveResponseObserver;
/**
* hasKeepAliveServiceStarted indicates whether the background keep alive service has started.
*/
private volatile boolean hasKeepAliveServiceStarted;
private volatile boolean closed;
LeaseImpl(ClientConnectionManager connectionManager) {
this.connectionManager = connectionManager;
this.stub = connectionManager.newStub(LeaseGrpc::newFutureStub);
this.leaseStub = Util.applyRequireLeader(true, connectionManager.newStub(LeaseGrpc::newStub));
this.keepAlives = new ConcurrentHashMap<>();
this.scheduledExecutorService = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(2));
}
@Override
public CompletableFuture<LeaseGrantResponse> grant(long ttl) {
return connectionManager.execute(() -> this.stub.leaseGrant(LeaseGrantRequest.newBuilder().setTTL(ttl).build()),
LeaseGrantResponse::new);
}
@Override
public CompletableFuture<LeaseGrantResponse> grant(long ttl, long timeout, TimeUnit unit) {
return connectionManager.execute(
() -> this.stub.withDeadlineAfter(timeout, unit).leaseGrant(LeaseGrantRequest.newBuilder().setTTL(ttl).build()),
LeaseGrantResponse::new);
}
@Override
public CompletableFuture<LeaseRevokeResponse> revoke(long leaseId) {
return connectionManager.execute(() -> this.stub.leaseRevoke(LeaseRevokeRequest.newBuilder().setID(leaseId).build()),
LeaseRevokeResponse::new);
}
@Override
public synchronized CloseableClient keepAlive(long leaseId, StreamObserver<LeaseKeepAliveResponse> observer) {
if (this.closed) {
throw newClosedLeaseClientException();
}
KeepAlive keepAlive = this.keepAlives.computeIfAbsent(leaseId, (key) -> new KeepAlive(leaseId));
keepAlive.addObserver(observer);
if (!this.hasKeepAliveServiceStarted) {
this.hasKeepAliveServiceStarted = true;
this.start();
}
return new CloseableClient() {
@Override
public void close() {
keepAlive.removeObserver(observer);
}
};
}
@Override
public synchronized void close() {
if (this.closed) {
return;
}
this.closed = true;
if (!this.hasKeepAliveServiceStarted) { // hasKeepAliveServiceStarted hasn't started.
return;
}
this.keepAliveFuture.cancel(true);
this.deadlineFuture.cancel(true);
this.keepAliveRequestObserver.onCompleted();
this.keepAliveResponseObserver.onCompleted();
this.scheduledExecutorService.shutdownNow();
final Throwable errResp = newClosedLeaseClientException();
this.keepAlives.forEach((k, v) -> v.onError(errResp));
this.keepAlives.clear();
}
private synchronized void removeKeepAlive(long leaseId) {
this.keepAlives.remove(leaseId);
}
private void start() {
this.sendKeepAliveExecutor();
this.deadLineExecutor();
}
private void reset() {
this.keepAliveFuture.cancel(true);
this.keepAliveRequestObserver.onCompleted();
this.keepAliveResponseObserver.onCompleted();
this.sendKeepAliveExecutor();
}
private void sendKeepAliveExecutor() {
this.keepAliveResponseObserver = Observers.observer(this::processKeepAliveResponse, error -> processOnError());
this.keepAliveRequestObserver = this.leaseStub.leaseKeepAlive(this.keepAliveResponseObserver);
this.keepAliveFuture = scheduledExecutorService.scheduleAtFixedRate(() -> {
// send keep alive req to the leases whose next keep alive is before now.
this.keepAlives.entrySet().stream()
.filter(entry -> entry.getValue().getNextKeepAlive() < System.currentTimeMillis()).map(Entry::getKey)
.map(leaseId -> LeaseKeepAliveRequest.newBuilder().setID(leaseId).build())
.forEach(keepAliveRequestObserver::onNext);
}, 0, 500, TimeUnit.MILLISECONDS);
}
private synchronized void processOnError() {
if (this.closed) {
return;
}
Futures.addCallback(this.scheduledExecutorService.schedule(this::reset, 500, TimeUnit.MILLISECONDS),
new FutureCallback<Object>() {
@Override
public void onFailure(Throwable throwable) {
LOG.error("scheduled reset failed", throwable);
}
@Override
public void onSuccess(Object result) {
}
}, this.scheduledExecutorService);
}
private synchronized void processKeepAliveResponse(io.etcd.jetcd.api.LeaseKeepAliveResponse leaseKeepAliveResponse) {
if (this.closed) {
return;
}
final long leaseID = leaseKeepAliveResponse.getID();
final long ttl = leaseKeepAliveResponse.getTTL();
final KeepAlive ka = this.keepAlives.get(leaseID);
if (ka == null) {
// return if the corresponding keep alive has closed.
return;
}
if (ttl > 0) {
long nextKeepAlive = System.currentTimeMillis() + ttl * 1000 / 3;
ka.setNextKeepAlive(nextKeepAlive);
ka.setDeadLine(System.currentTimeMillis() + ttl * 1000);
ka.onNext(leaseKeepAliveResponse);
} else {
// lease expired; close all keep alive
this.removeKeepAlive(leaseID);
ka.onError(newEtcdException(ErrorCode.NOT_FOUND, "etcdserver: requested lease not found"));
}
}
private void deadLineExecutor() {
this.deadlineFuture = scheduledExecutorService.scheduleAtFixedRate(() -> {
long now = System.currentTimeMillis();
this.keepAlives.values().removeIf(ka -> {
if (ka.getDeadLine() < now) {
ka.onCompleted();
return true;
}
return false;
});
}, 0, 1000, TimeUnit.MILLISECONDS);
}
@Override
public CompletableFuture<LeaseKeepAliveResponse> keepAliveOnce(long leaseId) {
CompletableFuture<LeaseKeepAliveResponse> future = new CompletableFuture<>();
StreamObserver<LeaseKeepAliveRequest> requestObserver = Observers.observe(
this.leaseStub::leaseKeepAlive,
response -> future.complete(new LeaseKeepAliveResponse(response)),
throwable -> future.completeExceptionally(toEtcdException(throwable)));
// cancel grpc stream when leaseKeepAliveResponseCompletableFuture completes.
CompletableFuture<LeaseKeepAliveResponse> answer = future
.whenCompleteAsync((val, throwable) -> requestObserver.onCompleted(), connectionManager.getExecutorService());
requestObserver.onNext(LeaseKeepAliveRequest.newBuilder().setID(leaseId).build());
return answer;
}
@Override
public CompletableFuture<LeaseTimeToLiveResponse> timeToLive(long leaseId, LeaseOption option) {
checkNotNull(option, "LeaseOption should not be null");
LeaseTimeToLiveRequest leaseTimeToLiveRequest = LeaseTimeToLiveRequest.newBuilder()
.setID(leaseId)
.setKeys(option.isAttachedKeys())
.build();
return connectionManager.execute(
() -> this.stub.leaseTimeToLive(leaseTimeToLiveRequest),
LeaseTimeToLiveResponse::new);
}
/**
* The KeepAlive hold the keepAlive information for lease.
*/
private final class KeepAlive implements StreamObserver<io.etcd.jetcd.api.LeaseKeepAliveResponse> {
private final List<StreamObserver<LeaseKeepAliveResponse>> observers;
private final long leaseId;
private long deadLine;
private long nextKeepAlive;
public KeepAlive(long leaseId) {
this.nextKeepAlive = System.currentTimeMillis();
this.deadLine = nextKeepAlive + FIRST_KEEPALIVE_TIMEOUT_MS;
this.observers = new CopyOnWriteArrayList<>();
this.leaseId = leaseId;
}
public long getDeadLine() {
return deadLine;
}
public void setDeadLine(long deadLine) {
this.deadLine = deadLine;
}
public void addObserver(StreamObserver<LeaseKeepAliveResponse> observer) {
this.observers.add(observer);
}
//removeObserver only would be called synchronously by close in KeepAliveListener, no need to get lock here
public void removeObserver(StreamObserver<LeaseKeepAliveResponse> listener) {
this.observers.remove(listener);
if (this.observers.isEmpty()) {
removeKeepAlive(leaseId);
}
}
public long getNextKeepAlive() {
return nextKeepAlive;
}
public void setNextKeepAlive(long nextKeepAlive) {
this.nextKeepAlive = nextKeepAlive;
}
@Override
public void onNext(io.etcd.jetcd.api.LeaseKeepAliveResponse response) {
for (StreamObserver<LeaseKeepAliveResponse> observer : observers) {
observer.onNext(new LeaseKeepAliveResponse(response));
}
}
@Override
public void onError(Throwable throwable) {
for (StreamObserver<LeaseKeepAliveResponse> observer : observers) {
observer.onError(toEtcdException(throwable));
}
}
@Override
public void onCompleted() {
this.observers.forEach(StreamObserver::onCompleted);
this.observers.clear();
}
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2016-2021 The jetcd 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.
*/
package io.etcd.jetcd;
import java.util.concurrent.CompletableFuture;
import io.etcd.jetcd.api.lock.LockGrpc;
import io.etcd.jetcd.api.lock.LockRequest;
import io.etcd.jetcd.api.lock.UnlockRequest;
import io.etcd.jetcd.lock.LockResponse;
import io.etcd.jetcd.lock.UnlockResponse;
import static com.google.common.base.Preconditions.checkNotNull;
final class LockImpl implements Lock {
private final ClientConnectionManager connectionManager;
private final LockGrpc.LockFutureStub stub;
private final ByteSequence namespace;
LockImpl(ClientConnectionManager connectionManager) {
this.connectionManager = connectionManager;
this.stub = connectionManager.newStub(LockGrpc::newFutureStub);
this.namespace = connectionManager.getNamespace();
}
@Override
public CompletableFuture<LockResponse> lock(ByteSequence name, long leaseId) {
checkNotNull(name);
LockRequest request = LockRequest.newBuilder()
.setName(Util.prefixNamespace(name.getByteString(), namespace))
.setLease(leaseId)
.build();
return connectionManager.execute(
() -> stub.lock(request),
response -> new LockResponse(response, namespace),
Util::isRetryable);
}
@Override
public CompletableFuture<UnlockResponse> unlock(ByteSequence lockKey) {
checkNotNull(lockKey);
UnlockRequest request = UnlockRequest.newBuilder()
.setKey(Util.prefixNamespace(lockKey.getByteString(), namespace))
.build();
return connectionManager.execute(
() -> stub.unlock(request),
UnlockResponse::new,
Util::isRetryable);
}
}

View File

@ -17,9 +17,16 @@
package io.etcd.jetcd;
import java.io.OutputStream;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import io.etcd.jetcd.maintenance.*;
import io.etcd.jetcd.maintenance.AlarmMember;
import io.etcd.jetcd.maintenance.AlarmResponse;
import io.etcd.jetcd.maintenance.DefragmentResponse;
import io.etcd.jetcd.maintenance.HashKVResponse;
import io.etcd.jetcd.maintenance.MoveLeaderResponse;
import io.etcd.jetcd.maintenance.SnapshotResponse;
import io.etcd.jetcd.maintenance.StatusResponse;
import io.etcd.jetcd.support.CloseableClient;
import io.grpc.stub.StreamObserver;
@ -61,6 +68,16 @@ public interface Maintenance extends CloseableClient {
*/
CompletableFuture<AlarmResponse> alarmDisarm(AlarmMember member);
/**
* Defragment one member of the cluster by its endpoint.
*
* @param endpoint the etcd server endpoint.
* @return the response result
* @deprecated use {@link #defragmentMember(String)}
*/
@Deprecated
CompletableFuture<DefragmentResponse> defragmentMember(URI endpoint);
/**
* Defragment one member of the cluster by its endpoint.
*
@ -83,6 +100,16 @@ public interface Maintenance extends CloseableClient {
*/
CompletableFuture<DefragmentResponse> defragmentMember(String target);
/**
* get the status of a member by its endpoint.
*
* @param endpoint the etcd server endpoint.
* @return the response result
* @deprecated use {@link #statusMember(String)}
*/
@Deprecated
CompletableFuture<StatusResponse> statusMember(URI endpoint);
/**
* get the status of a member by its endpoint.
*
@ -91,6 +118,17 @@ public interface Maintenance extends CloseableClient {
*/
CompletableFuture<StatusResponse> statusMember(String target);
/**
* returns a hash of the KV state at the time of the RPC.
*
* @param endpoint the etcd server endpoint.
* @param rev the revision
* @return the response result
* @deprecated use {@link #hashKV(String, long)}
*/
@Deprecated
CompletableFuture<HashKVResponse> hashKV(URI endpoint, long rev);
/**
* returns a hash of the KV state at the time of the RPC.
* If revision is zero, the hash is computed on all keys. If the revision

View File

@ -0,0 +1,213 @@
/*
* Copyright 2016-2021 The jetcd 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.
*/
package io.etcd.jetcd;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;
import io.etcd.jetcd.api.AlarmRequest;
import io.etcd.jetcd.api.AlarmType;
import io.etcd.jetcd.api.DefragmentRequest;
import io.etcd.jetcd.api.HashKVRequest;
import io.etcd.jetcd.api.MaintenanceGrpc;
import io.etcd.jetcd.api.MoveLeaderRequest;
import io.etcd.jetcd.api.SnapshotRequest;
import io.etcd.jetcd.api.SnapshotResponse;
import io.etcd.jetcd.api.StatusRequest;
import io.etcd.jetcd.maintenance.AlarmResponse;
import io.etcd.jetcd.maintenance.DefragmentResponse;
import io.etcd.jetcd.maintenance.HashKVResponse;
import io.etcd.jetcd.maintenance.MoveLeaderResponse;
import io.etcd.jetcd.maintenance.StatusResponse;
import io.grpc.stub.StreamObserver;
import static com.google.common.base.Preconditions.checkArgument;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.toEtcdException;
/**
* Implementation of maintenance client.
*/
final class MaintenanceImpl implements Maintenance {
private final ClientConnectionManager connectionManager;
private final MaintenanceGrpc.MaintenanceFutureStub stub;
private final MaintenanceGrpc.MaintenanceStub streamStub;
MaintenanceImpl(ClientConnectionManager connectionManager) {
this.connectionManager = connectionManager;
this.stub = connectionManager.newStub(MaintenanceGrpc::newFutureStub);
this.streamStub = connectionManager.newStub(MaintenanceGrpc::newStub);
}
@Override
public CompletableFuture<AlarmResponse> listAlarms() {
AlarmRequest alarmRequest = AlarmRequest.newBuilder()
.setAlarm(AlarmType.NONE)
.setAction(AlarmRequest.AlarmAction.GET)
.setMemberID(0)
.build();
return Util.toCompletableFuture(this.stub.alarm(alarmRequest), AlarmResponse::new,
this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<AlarmResponse> alarmDisarm(io.etcd.jetcd.maintenance.AlarmMember member) {
checkArgument(member.getMemberId() != 0, "the member id can not be 0");
checkArgument(member.getAlarmType() != io.etcd.jetcd.maintenance.AlarmType.NONE, "alarm type can not be NONE");
AlarmRequest alarmRequest = AlarmRequest.newBuilder()
.setAlarm(AlarmType.NOSPACE)
.setAction(AlarmRequest.AlarmAction.DEACTIVATE)
.setMemberID(member.getMemberId())
.build();
return Util.toCompletableFuture(this.stub.alarm(alarmRequest), AlarmResponse::new,
this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<DefragmentResponse> defragmentMember(URI endpoint) {
return this.connectionManager.withNewChannel(
// TODO
endpoint.toString(),
MaintenanceGrpc::newFutureStub,
stub -> Util.toCompletableFuture(
stub.defragment(DefragmentRequest.getDefaultInstance()),
DefragmentResponse::new,
this.connectionManager.getExecutorService()));
}
@Override
public CompletableFuture<DefragmentResponse> defragmentMember(String target) {
return this.connectionManager.withNewChannel(
target,
MaintenanceGrpc::newFutureStub,
stub -> Util.toCompletableFuture(
stub.defragment(DefragmentRequest.getDefaultInstance()),
DefragmentResponse::new,
this.connectionManager.getExecutorService()));
}
@Override
public CompletableFuture<StatusResponse> statusMember(URI endpoint) {
return this.connectionManager.withNewChannel(
// TODO
endpoint.toString(),
MaintenanceGrpc::newFutureStub,
stub -> Util.toCompletableFuture(
stub.status(StatusRequest.getDefaultInstance()),
StatusResponse::new,
this.connectionManager.getExecutorService()));
}
@Override
public CompletableFuture<StatusResponse> statusMember(String target) {
return this.connectionManager.withNewChannel(
target,
MaintenanceGrpc::newFutureStub,
stub -> Util.toCompletableFuture(
stub.status(StatusRequest.getDefaultInstance()),
StatusResponse::new,
this.connectionManager.getExecutorService()));
}
@Override
public CompletableFuture<MoveLeaderResponse> moveLeader(long transfereeID) {
return Util.toCompletableFuture(
this.stub.moveLeader(MoveLeaderRequest.newBuilder().setTargetID(transfereeID).build()),
MoveLeaderResponse::new,
this.connectionManager.getExecutorService());
}
@Override
public CompletableFuture<HashKVResponse> hashKV(URI endpoint, long rev) {
return this.connectionManager.withNewChannel(
// TODO
endpoint.toString(),
MaintenanceGrpc::newFutureStub,
stub -> Util.toCompletableFuture(
stub.hashKV(HashKVRequest.newBuilder().setRevision(rev).build()),
HashKVResponse::new,
this.connectionManager.getExecutorService()));
}
@Override
public CompletableFuture<HashKVResponse> hashKV(String target, long rev) {
return this.connectionManager.withNewChannel(
target,
MaintenanceGrpc::newFutureStub,
stub -> Util.toCompletableFuture(
stub.hashKV(HashKVRequest.newBuilder().setRevision(rev).build()),
HashKVResponse::new,
this.connectionManager.getExecutorService()));
}
@Override
public CompletableFuture<Long> snapshot(OutputStream outputStream) {
final CompletableFuture<Long> answer = new CompletableFuture<>();
final AtomicLong bytes = new AtomicLong(0);
this.streamStub.snapshot(SnapshotRequest.getDefaultInstance(), new StreamObserver<SnapshotResponse>() {
@Override
public void onNext(SnapshotResponse snapshotResponse) {
try {
snapshotResponse.getBlob().writeTo(outputStream);
bytes.addAndGet(snapshotResponse.getBlob().size());
} catch (IOException e) {
answer.completeExceptionally(toEtcdException(e));
}
}
@Override
public void onError(Throwable throwable) {
answer.completeExceptionally(toEtcdException(throwable));
}
@Override
public void onCompleted() {
answer.complete(bytes.get());
}
});
return answer;
}
@Override
public void snapshot(StreamObserver<io.etcd.jetcd.maintenance.SnapshotResponse> observer) {
this.streamStub.snapshot(SnapshotRequest.getDefaultInstance(), new StreamObserver<SnapshotResponse>() {
@Override
public void onNext(SnapshotResponse snapshotResponse) {
observer.onNext(new io.etcd.jetcd.maintenance.SnapshotResponse(snapshotResponse));
}
@Override
public void onError(Throwable throwable) {
observer.onError(toEtcdException(throwable));
}
@Override
public void onCompleted() {
observer.onCompleted();
}
});
}
}

View File

@ -1,32 +0,0 @@
/*
* Copyright 2016-2023 The jetcd 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.
*/
package io.etcd.jetcd;
public class Preconditions {
public static void checkArgument(boolean expression, String errorMessage) {
if (!expression) {
throw new IllegalArgumentException(errorMessage);
}
}
public static void checkState(boolean expression, String errorMessage) {
if (!expression) {
throw new IllegalStateException(errorMessage);
}
}
}

View File

@ -23,8 +23,6 @@ public interface Response {
/**
* Returns the response header
*
* @return the header.
*/
Header getHeader();
@ -32,29 +30,21 @@ public interface Response {
/**
* Returns the cluster id
*
* @return the cluster id.
*/
long getClusterId();
/**
* Returns the member id
*
* @return the member id.
*/
long getMemberId();
/**
* Returns the revision id
*
* @return the revision.
*/
long getRevision();
/**
* Returns the raft term
*
* @return theraft term.
*/
long getRaftTerm();
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2016-2023 The jetcd authors
* Copyright 2016-2021 The jetcd authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -25,19 +25,20 @@ import io.etcd.jetcd.op.Op;
/**
* Txn is the interface that wraps mini-transactions.
*
* <h2>Usage examples</h2>
* <h3>Usage examples</h3>
*
* <pre>
* {@code
* txn.If(
* new Cmp(KEY, Cmp.Op.GREATER, CmpTarget.value(VALUE)),
* new Cmp(KEY, cmp.Op.EQUAL, CmpTarget.version(2))).Then(
* Op.put(KEY2, VALUE2, PutOption.DEFAULT),
* Op.put(KEY3, VALUE3, PutOption.DEFAULT))
* .Else(
* Op.put(KEY4, VALUE4, PutOption.DEFAULT),
* Op.put(KEY4, VALUE4, PutOption.DEFAULT))
* .commit();
* new Cmp(KEY, Cmp.Op.GREATER, CmpTarget.value(VALUE)),
* new Cmp(KEY, cmp.Op.EQUAL, CmpTarget.version(2))
* ).Then(
* Op.put(KEY2, VALUE2, PutOption.DEFAULT),
* Op.put(KEY3, VALUE3, PutOption.DEFAULT)
* ).Else(
* Op.put(KEY4, VALUE4, PutOption.DEFAULT),
* Op.put(KEY4, VALUE4, PutOption.DEFAULT)
* ).commit();
* }
* </pre>
*
@ -47,17 +48,18 @@ import io.etcd.jetcd.op.Op;
* <pre>
* {@code
* txn.If(
* new Cmp(KEY, Cmp.Op.GREATER, CmpTarget.value(VALUE))).If(
* new Cmp(KEY, cmp.Op.EQUAL, CmpTarget.version(VERSION)))
* .Then(
* Op.put(KEY2, VALUE2, PutOption.DEFAULT))
* .Then(
* Op.put(KEY3, VALUE3, PutOption.DEFAULT))
* .Else(
* Op.put(KEY4, VALUE4, PutOption.DEFAULT))
* .Else(
* Op.put(KEY4, VALUE4, PutOption.DEFAULT))
* .commit();
* new Cmp(KEY, Cmp.Op.GREATER, CmpTarget.value(VALUE))
* ).If(
* new Cmp(KEY, cmp.Op.EQUAL, CmpTarget.version(VERSION))
* ).Then(
* Op.put(KEY2, VALUE2, PutOption.DEFAULT)
* ).Then(
* Op.put(KEY3, VALUE3, PutOption.DEFAULT)
* ).Else(
* Op.put(KEY4, VALUE4, PutOption.DEFAULT)
* ).Else(
* Op.put(KEY4, VALUE4, PutOption.DEFAULT)
* ).commit();
* }
* </pre>
*/

View File

@ -14,23 +14,26 @@
* limitations under the License.
*/
package io.etcd.jetcd.support;
package io.etcd.jetcd;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Constants;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.stub.AbstractStub;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.toEtcdException;
import static io.grpc.stub.MetadataUtils.newAttachHeadersInterceptor;
public final class Util {
@ -48,38 +51,48 @@ public final class Util {
}).collect(Collectors.toList());
}
public static ByteString prefixNamespace(ByteSequence key, ByteSequence namespace) {
return ByteString.copyFrom(namespace.isEmpty() ? key.getBytes() : namespace.concat(key).getBytes());
/**
* convert ListenableFuture of Type S to CompletableFuture of Type T.
*/
static <S, T> CompletableFuture<T> toCompletableFuture(ListenableFuture<S> sourceFuture, Function<S, T> resultConvert,
Executor executor) {
CompletableFuture<T> targetFuture = new CompletableFuture<T>() {
// the cancel of targetFuture also cancels the sourceFuture.
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
super.cancel(mayInterruptIfRunning);
return sourceFuture.cancel(mayInterruptIfRunning);
}
};
sourceFuture.addListener(() -> {
try {
targetFuture.complete(resultConvert.apply(sourceFuture.get()));
} catch (Exception e) {
targetFuture.completeExceptionally(toEtcdException(e));
}
}, executor);
return targetFuture;
}
public static boolean isRetryable(Throwable e) {
Status status = Status.fromThrowable(e);
return Status.UNAVAILABLE.getCode().equals(status.getCode()) || isInvalidTokenError(status);
}
public static boolean isInvalidTokenError(Status status) {
return status.getCode() == Status.Code.UNAUTHENTICATED
&& "etcdserver: invalid auth token".equals(status.getDescription());
}
public static <T> T supplyIfNull(T target, Supplier<T> supplier) {
return target != null ? target : supplier.get();
}
public static ByteString prefixNamespace(ByteString key, ByteSequence namespace) {
return namespace.isEmpty() ? key : ByteString.copyFrom(namespace.concat(key).getBytes());
}
public static ByteString prefixNamespaceToRangeEnd(ByteSequence end, ByteSequence namespace) {
if (namespace.isEmpty()) {
return ByteString.copyFrom(end.getBytes());
}
if (end.size() == 1 && end.getBytes()[0] == 0) {
// range end is '\0', calculate the prefixed range end by (key + 1)
byte[] prefixedEndArray = namespace.getBytes();
boolean ok = false;
for (int i = prefixedEndArray.length - 1; i >= 0; i--) {
prefixedEndArray[i] = (byte) (prefixedEndArray[i] + 1);
if (prefixedEndArray[i] != 0) {
ok = true;
break;
}
}
if (!ok) {
// 0xff..ff => 0x00
prefixedEndArray = Constants.NULL_KEY.getBytes();
}
return ByteString.copyFrom(prefixedEndArray);
} else {
return ByteString.copyFrom(namespace.concat(end).getBytes());
}
return namespace.isEmpty() ? key : namespace.getByteString().concat(key);
}
public static ByteString prefixNamespaceToRangeEnd(ByteString end, ByteSequence namespace) {
@ -89,7 +102,7 @@ public final class Util {
if (end.size() == 1 && end.toByteArray()[0] == 0) {
// range end is '\0', calculate the prefixed range end by (key + 1)
byte[] prefixedEndArray = namespace.getBytes();
byte[] prefixedEndArray = namespace.getByteString().toByteArray();
boolean ok = false;
for (int i = prefixedEndArray.length - 1; i >= 0; i--) {
prefixedEndArray[i] = (byte) (prefixedEndArray[i] + 1);
@ -100,11 +113,11 @@ public final class Util {
}
if (!ok) {
// 0xff..ff => 0x00
prefixedEndArray = Constants.NULL_KEY.getBytes();
prefixedEndArray = new byte[] { 0 };
}
return ByteString.copyFrom(prefixedEndArray);
} else {
return ByteString.copyFrom(namespace.concat(end).getBytes());
return namespace.getByteString().concat(end);
}
}
@ -112,7 +125,7 @@ public final class Util {
return namespace.isEmpty() ? key : key.substring(namespace.size());
}
public static <T extends AbstractStub<T>> T applyRequireLeader(boolean requireLeader, T stub) {
static <T extends AbstractStub<T>> T applyRequireLeader(boolean requireLeader, T stub) {
if (!requireLeader) {
return stub;
}
@ -121,15 +134,13 @@ public final class Util {
return stub.withInterceptors(newAttachHeadersInterceptor(md));
}
public static ThreadFactory createThreadFactory(String prefix, boolean daemon) {
ThreadFactory backingThreadFactory = Executors.defaultThreadFactory();
public static boolean isHaltError(final Status status) {
return status.getCode() != Status.Code.UNAVAILABLE && status.getCode() != Status.Code.INTERNAL;
}
return r -> {
Thread t = backingThreadFactory.newThread(r);
t.setDaemon(daemon);
// set a proper name so it is easier to find out the where the thread was created
t.setName(prefix + t.getName());
return t;
};
static final String NO_LEADER_ERROR_MESSAGE = "etcdserver: no leader";
public static boolean isNoLeaderError(final Status status) {
return status.getCode() == Status.Code.UNAVAILABLE && NO_LEADER_ERROR_MESSAGE.equals(status.getDescription());
}
}

View File

@ -226,11 +226,6 @@ public interface Watch extends CloseableClient {
@Override
void close();
/**
* Returns if watcher is already closed
*/
boolean isClosed();
/**
* Requests the latest revision processed and propagates it to listeners
*/

View File

@ -14,23 +14,20 @@
* limitations under the License.
*/
package io.etcd.jetcd.impl;
package io.etcd.jetcd;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Watch;
import io.etcd.jetcd.api.VertxWatchGrpc;
import io.etcd.jetcd.api.WatchCancelRequest;
import io.etcd.jetcd.api.WatchCreateRequest;
import io.etcd.jetcd.api.WatchGrpc;
import io.etcd.jetcd.api.WatchProgressRequest;
import io.etcd.jetcd.api.WatchRequest;
import io.etcd.jetcd.api.WatchResponse;
@ -38,10 +35,8 @@ import io.etcd.jetcd.common.exception.ErrorCode;
import io.etcd.jetcd.common.exception.EtcdException;
import io.etcd.jetcd.options.OptionsUtil;
import io.etcd.jetcd.options.WatchOption;
import io.etcd.jetcd.support.Errors;
import io.etcd.jetcd.support.Util;
import io.grpc.Status;
import io.vertx.core.streams.WriteStream;
import io.grpc.stub.StreamObserver;
import com.google.common.base.Strings;
import com.google.common.util.concurrent.FutureCallback;
@ -57,24 +52,22 @@ import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.toEtcdExceptio
/**
* watch Implementation.
*/
final class WatchImpl extends Impl implements Watch {
final class WatchImpl implements Watch {
private static final Logger LOG = LoggerFactory.getLogger(WatchImpl.class);
private final Object lock;
private final VertxWatchGrpc.WatchVertxStub stub;
private final ClientConnectionManager connectionManager;
private final WatchGrpc.WatchStub stub;
private final ListeningScheduledExecutorService executor;
private final AtomicBoolean closed;
private final List<WatcherImpl> watchers;
private final ByteSequence namespace;
WatchImpl(ClientConnectionManager connectionManager) {
super(connectionManager);
this.lock = new Object();
this.stub = connectionManager.newStub(VertxWatchGrpc::newVertxStub);
// set it to daemon as there is no way for users to create this thread pool by their own
this.executor = MoreExecutors.listeningDecorator(
Executors.newScheduledThreadPool(1, Util.createThreadFactory("jetcd-watch-", true)));
this.connectionManager = connectionManager;
this.stub = connectionManager.newStub(WatchGrpc::newStub);
this.executor = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(1));
this.closed = new AtomicBoolean();
this.watchers = new CopyOnWriteArrayList<>();
this.namespace = connectionManager.getNamespace();
@ -117,14 +110,13 @@ final class WatchImpl extends Impl implements Watch {
}
}
final class WatcherImpl implements Watcher {
final class WatcherImpl implements Watcher, StreamObserver<WatchResponse> {
private final ByteSequence key;
private final WatchOption option;
private final Listener listener;
private final AtomicBoolean closed;
private final AtomicReference<WriteStream<WatchRequest>> wstream;
private final AtomicBoolean started;
private StreamObserver<WatchRequest> stream;
private long revision;
private long id;
@ -134,8 +126,7 @@ final class WatchImpl extends Impl implements Watch {
this.listener = listener;
this.closed = new AtomicBoolean();
this.started = new AtomicBoolean();
this.wstream = new AtomicReference<>();
this.stream = null;
this.id = -1;
this.revision = this.option.getRevision();
}
@ -146,8 +137,7 @@ final class WatchImpl extends Impl implements Watch {
//
// ************************
@Override
public boolean isClosed() {
boolean isClosed() {
return this.closed.get() || WatchImpl.this.closed.get();
}
@ -156,22 +146,20 @@ final class WatchImpl extends Impl implements Watch {
return;
}
if (started.compareAndSet(false, true)) {
// id is not really useful today, but it may be in etcd 3.4
if (stream == null) {
// id is not really useful today but it may be in etcd 3.4
id = -1;
WatchCreateRequest.Builder builder = WatchCreateRequest.newBuilder()
.setKey(Util.prefixNamespace(this.key, namespace))
.setPrevKv(this.option.isPrevKV())
.setKey(Util.prefixNamespace(this.key.getByteString(), namespace)).setPrevKv(this.option.isPrevKV())
.setProgressNotify(option.isProgressNotify()).setStartRevision(this.revision);
option.getEndKey()
.map(endKey -> Util.prefixNamespaceToRangeEnd(endKey, namespace))
option.getEndKey().map(endKey -> Util.prefixNamespaceToRangeEnd(endKey.getByteString(), namespace))
.ifPresent(builder::setRangeEnd);
if (option.getEndKey().isEmpty() && option.isPrefix()) {
if (!option.getEndKey().isPresent() && option.isPrefix()) {
ByteSequence endKey = OptionsUtil.prefixEndOf(key);
builder.setRangeEnd(Util.prefixNamespaceToRangeEnd(endKey, namespace));
builder.setRangeEnd(Util.prefixNamespaceToRangeEnd(endKey.getByteString(), namespace));
}
if (option.isNoDelete()) {
@ -182,15 +170,8 @@ final class WatchImpl extends Impl implements Watch {
builder.addFilters(WatchCreateRequest.FilterType.NOPUT);
}
var ignored = Util.applyRequireLeader(option.withRequireLeader(), stub)
.watchWithHandler(
stream -> {
wstream.set(stream);
stream.write(WatchRequest.newBuilder().setCreateRequest(builder).build());
},
this::onNext,
event -> onCompleted(),
this::onError);
stream = Util.applyRequireLeader(option.withRequireLeader(), stub).watch(this);
stream.onNext(WatchRequest.newBuilder().setCreateRequest(builder).build());
}
}
@ -200,15 +181,16 @@ final class WatchImpl extends Impl implements Watch {
// sync with onError()
synchronized (WatchImpl.this.lock) {
if (closed.compareAndSet(false, true)) {
if (wstream.get() != null) {
if (id != -1) {
final WatchCancelRequest watchCancelRequest = WatchCancelRequest.newBuilder().setWatchId(this.id)
.build();
final WatchRequest request = WatchRequest.newBuilder().setCancelRequest(watchCancelRequest).build();
if (stream != null) {
WatchCancelRequest watchCancelRequest = WatchCancelRequest.newBuilder().setWatchId(this.id).build();
wstream.get().end(request);
} else {
wstream.get().end();
if (id != -1) {
stream.onNext(WatchRequest.newBuilder().setCancelRequest(watchCancelRequest).build());
}
if (stream != null) {
stream.onError(Status.CANCELLED.withDescription("shutdown").asException());
stream = null;
}
}
@ -224,9 +206,9 @@ final class WatchImpl extends Impl implements Watch {
@Override
public void requestProgress() {
if (!closed.get() && wstream.get() != null) {
if (!closed.get() && stream != null) {
WatchProgressRequest watchProgressRequest = WatchProgressRequest.newBuilder().build();
wstream.get().write(WatchRequest.newBuilder().setProgressRequest(watchProgressRequest).build());
stream.onNext(WatchRequest.newBuilder().setProgressRequest(watchProgressRequest).build());
}
}
@ -236,7 +218,8 @@ final class WatchImpl extends Impl implements Watch {
//
// ************************
private void onNext(WatchResponse response) {
@Override
public void onNext(WatchResponse response) {
if (closed.get()) {
// events eventually received when the client is closed should
// not be propagated to the listener
@ -245,11 +228,9 @@ final class WatchImpl extends Impl implements Watch {
// handle a special case when watch has been created and closed at the same time
if (response.getCreated() && response.getCanceled() && response.getCancelReason() != null
&& (response.getCancelReason().contains("etcdserver: permission denied") ||
response.getCancelReason().contains("etcdserver: invalid auth token"))) {
&& response.getCancelReason().contains("etcdserver: permission denied")) {
// potentially access token expired
connectionManager().authCredential().refresh();
connectionManager.authInterceptor().refresh();
Status error = Status.Code.CANCELLED.toStatus().withDescription(response.getCancelReason());
handleError(toEtcdException(error), true);
} else if (response.getCreated()) {
@ -265,9 +246,6 @@ final class WatchImpl extends Impl implements Watch {
revision = Math.max(revision, response.getHeader().getRevision());
id = response.getWatchId();
if (option.isCreatedNotify()) {
listener.onNext(new io.etcd.jetcd.watch.WatchResponse(response));
}
} else if (response.getCanceled()) {
//
@ -315,11 +293,8 @@ final class WatchImpl extends Impl implements Watch {
}
}
private void onCompleted() {
listener.onCompleted();
}
private void onError(Throwable t) {
@Override
public void onError(Throwable t) {
handleError(toEtcdException(t), shouldReschedule(Status.fromThrowable(t)));
}
@ -331,18 +306,12 @@ final class WatchImpl extends Impl implements Watch {
}
listener.onError(etcdException);
if (wstream.get() != null) {
wstream.get().end();
if (stream != null) {
stream.onCompleted();
}
wstream.set(null);
started.set(false);
stream = null;
}
if (shouldReschedule) {
if (etcdException.getMessage().contains("etcdserver: permission denied")) {
// potentially access token expired
connectionManager().authCredential().refresh();
}
reschedule();
return;
}
@ -350,14 +319,14 @@ final class WatchImpl extends Impl implements Watch {
}
private boolean shouldReschedule(final Status status) {
return !Errors.isHaltError(status) && !Errors.isNoLeaderError(status);
return !Util.isHaltError(status) && !Util.isNoLeaderError(status);
}
private void reschedule() {
Futures.addCallback(executor.schedule(this::resume, 500, TimeUnit.MILLISECONDS), new FutureCallback<Object>() {
@Override
public void onFailure(Throwable t) {
LOG.warn("scheduled resume failed", t);
public void onFailure(Throwable throwable) {
LOG.error("scheduled resume failed", throwable);
}
@Override
@ -365,5 +334,9 @@ final class WatchImpl extends Impl implements Watch {
}
}, executor);
}
@Override
public void onCompleted() {
}
}
}

View File

@ -16,11 +16,10 @@
package io.etcd.jetcd.auth;
import io.etcd.jetcd.Auth;
import io.etcd.jetcd.impl.AbstractResponse;
import io.etcd.jetcd.AbstractResponse;
/**
* AuthDisableResponse returned by {@link Auth#authDisable()} contains a header.
* AuthDisableResponse returned by {@link io.etcd.jetcd.Auth#authDisable()} contains a header.
*/
public class AuthDisableResponse extends AbstractResponse<io.etcd.jetcd.api.AuthDisableResponse> {

View File

@ -16,11 +16,10 @@
package io.etcd.jetcd.auth;
import io.etcd.jetcd.Auth;
import io.etcd.jetcd.impl.AbstractResponse;
import io.etcd.jetcd.AbstractResponse;
/**
* AuthEnableResponse returned by {@link Auth#authEnable()} call contains a header.
* AuthEnableResponse returned by {@link io.etcd.jetcd.Auth#authEnable()} call contains a header.
*/
public class AuthEnableResponse extends AbstractResponse<io.etcd.jetcd.api.AuthEnableResponse> {

View File

@ -0,0 +1,180 @@
/*
* Copyright 2016-2021 The jetcd 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.
*/
package io.etcd.jetcd.auth;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import io.etcd.jetcd.ClientBuilder;
import io.etcd.jetcd.api.AuthGrpc;
import io.etcd.jetcd.api.AuthenticateRequest;
import io.etcd.jetcd.api.AuthenticateResponse;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall;
import io.grpc.ForwardingClientCallListener;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import io.grpc.stub.MetadataUtils;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString;
import static com.google.common.base.Preconditions.checkArgument;
import static io.etcd.jetcd.Util.isInvalidTokenError;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.handleInterrupt;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.toEtcdException;
/**
* AuthTokenInterceptor fills header with Auth token of any rpc calls and
* refreshes token if the rpc results an invalid Auth token error.
*/
public class AuthInterceptor implements ClientInterceptor {
private static final Metadata.Key<String> TOKEN = Metadata.Key.of("token", Metadata.ASCII_STRING_MARSHALLER);
private final Object lock;
private final ClientBuilder builder;
private final ClientInterceptor[] interceptors;
private volatile String token;
public AuthInterceptor(ClientBuilder builder) {
this.lock = new Object();
this.builder = builder;
List<ClientInterceptor> interceptorsChain = new ArrayList<>();
if (builder.authHeaders() != null) {
Metadata metadata = new Metadata();
builder.authHeaders().forEach((BiConsumer<Metadata.Key, Object>) metadata::put);
interceptorsChain.add(MetadataUtils.newAttachHeadersInterceptor(metadata));
}
if (builder.authInterceptors() != null) {
interceptorsChain.addAll(builder.authInterceptors());
}
this.interceptors = interceptorsChain.isEmpty() ? null : interceptorsChain.toArray(new ClientInterceptor[0]);
}
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
String token = getToken(next);
if (token != null) {
headers.put(TOKEN, token);
}
super.start(new ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT>(responseListener) {
@Override
public void onClose(Status status, Metadata trailers) {
if (isInvalidTokenError(status)) {
try {
refreshToken(next);
} catch (Exception e) {
// don't throw any error here.
// rpc will retry on expired auth token.
}
}
super.onClose(status, trailers);
}
}, headers);
}
};
}
public void refresh() {
synchronized (lock) {
token = null;
}
}
/**
* get token from etcd with name and password.
*
* @param channel channel to etcd
* @return authResp
*/
@SuppressWarnings("rawtypes")
private ListenableFuture<AuthenticateResponse> authenticate(@Nonnull Channel channel) {
final ByteString user = ByteString.copyFrom(builder.user().getBytes());
final ByteString pass = ByteString.copyFrom(builder.password().getBytes());
checkArgument(!user.isEmpty(), "username can not be empty.");
checkArgument(!pass.isEmpty(), "password can not be empty.");
AuthGrpc.AuthFutureStub authFutureStub = AuthGrpc.newFutureStub(channel);
if (interceptors != null) {
authFutureStub = authFutureStub.withInterceptors(interceptors);
}
return authFutureStub.authenticate(
AuthenticateRequest.newBuilder()
.setNameBytes(user)
.setPasswordBytes(pass)
.build());
}
@Nullable
private String getToken(Channel channel) {
if (token == null) {
synchronized (lock) {
if (token == null) {
token = generateToken(channel);
}
}
}
return token;
}
private void refreshToken(Channel channel) {
synchronized (lock) {
token = generateToken(channel);
}
}
/**
* get token with ClientBuilder.
*
* @return the auth token
* @throws io.etcd.jetcd.common.exception.EtcdException a exception indicates failure reason.
*/
@Nullable
private String generateToken(Channel channel) {
if (builder.user() != null && builder.password() != null) {
try {
return authenticate(channel).get().getToken();
} catch (InterruptedException ite) {
throw handleInterrupt(ite);
} catch (ExecutionException exee) {
throw toEtcdException(exee);
}
}
return null;
}
}

View File

@ -16,12 +16,11 @@
package io.etcd.jetcd.auth;
import io.etcd.jetcd.Auth;
import io.etcd.jetcd.AbstractResponse;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.impl.AbstractResponse;
/**
* AuthRoleAddResponse returned by {@link Auth#roleAdd(ByteSequence)} contains
* AuthRoleAddResponse returned by {@link io.etcd.jetcd.Auth#roleAdd(ByteSequence)} contains
* a header.
*/
public class AuthRoleAddResponse extends AbstractResponse<io.etcd.jetcd.api.AuthRoleAddResponse> {

View File

@ -16,12 +16,11 @@
package io.etcd.jetcd.auth;
import io.etcd.jetcd.Auth;
import io.etcd.jetcd.AbstractResponse;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.impl.AbstractResponse;
/**
* AuthRoleDeleteResponse returned by {@link Auth#roleDelete(ByteSequence)}
* AuthRoleDeleteResponse returned by {@link io.etcd.jetcd.Auth#roleDelete(ByteSequence)}
* contains a header.
*/
public class AuthRoleDeleteResponse extends AbstractResponse<io.etcd.jetcd.api.AuthRoleDeleteResponse> {

View File

@ -19,12 +19,11 @@ package io.etcd.jetcd.auth;
import java.util.List;
import java.util.stream.Collectors;
import io.etcd.jetcd.Auth;
import io.etcd.jetcd.AbstractResponse;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.impl.AbstractResponse;
/**
* AuthRoleGetResponse returned by {@link Auth#roleGet(ByteSequence)} contains
* AuthRoleGetResponse returned by {@link io.etcd.jetcd.Auth#roleGet(ByteSequence)} contains
* a header and a list of permissions.
*/
public class AuthRoleGetResponse extends AbstractResponse<io.etcd.jetcd.api.AuthRoleGetResponse> {

View File

@ -16,13 +16,12 @@
package io.etcd.jetcd.auth;
import io.etcd.jetcd.Auth;
import io.etcd.jetcd.AbstractResponse;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.impl.AbstractResponse;
/**
* AuthRoleGrantPermissionResponse returned by
* {@link Auth#roleGrantPermission(ByteSequence, ByteSequence, ByteSequence, Permission.Type)} contains a
* {@link io.etcd.jetcd.Auth#roleGrantPermission(ByteSequence, ByteSequence, ByteSequence, Permission.Type)} contains a
* header.
*/
public class AuthRoleGrantPermissionResponse extends AbstractResponse<io.etcd.jetcd.api.AuthRoleGrantPermissionResponse> {

View File

@ -18,8 +18,8 @@ package io.etcd.jetcd.auth;
import java.util.List;
import io.etcd.jetcd.AbstractResponse;
import io.etcd.jetcd.Auth;
import io.etcd.jetcd.impl.AbstractResponse;
/**
* AuthRoleListResponse returned by {@link Auth#roleList()} contains a header and
@ -33,8 +33,6 @@ public class AuthRoleListResponse extends AbstractResponse<io.etcd.jetcd.api.Aut
/**
* Returns a list of roles.
*
* @return the roles.
*/
public List<String> getRoles() {
return getResponse().getRolesList();

View File

@ -16,10 +16,12 @@
package io.etcd.jetcd.auth;
import io.etcd.jetcd.impl.AbstractResponse;
import io.etcd.jetcd.AbstractResponse;
/**
* AuthRoleRevokePermissionResponse
* AuthRoleRevokePermissionResponse returned by
* {@link io.etcd.jetcd.Auth#roleRevokePermission(io.etcd.jetcd.ByteSequence.ByteSequence,
* io.etcd.jetcd.ByteSequence.ByteSequence, io.etcd.jetcd.ByteSequence.ByteSequence)} contains a header.
*/
public class AuthRoleRevokePermissionResponse extends AbstractResponse<io.etcd.jetcd.api.AuthRoleRevokePermissionResponse> {

View File

@ -16,12 +16,11 @@
package io.etcd.jetcd.auth;
import io.etcd.jetcd.Auth;
import io.etcd.jetcd.AbstractResponse;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.impl.AbstractResponse;
/**
* AuthUserAddResponse returned by {@link Auth#userAdd(ByteSequence, ByteSequence)} contains a
* AuthUserAddResponse returned by {@link io.etcd.jetcd.Auth#userAdd(ByteSequence, ByteSequence)} contains a
* header.
*/
public class AuthUserAddResponse extends AbstractResponse<io.etcd.jetcd.api.AuthUserAddResponse> {

View File

@ -16,10 +16,12 @@
package io.etcd.jetcd.auth;
import io.etcd.jetcd.impl.AbstractResponse;
import io.etcd.jetcd.AbstractResponse;
/**
* AuthUserChangePasswordResponse
* AuthUserChangePasswordResponse returned by
* {@link io.etcd.jetcd.Auth#userChangePassword(io.etcd.jetcd.ByteSequence.ByteSequence,
* io.etcd.jetcd.ByteSequence.ByteSequence)} contains a header.
*/
public class AuthUserChangePasswordResponse extends AbstractResponse<io.etcd.jetcd.api.AuthUserChangePasswordResponse> {

View File

@ -16,12 +16,11 @@
package io.etcd.jetcd.auth;
import io.etcd.jetcd.Auth;
import io.etcd.jetcd.AbstractResponse;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.impl.AbstractResponse;
/**
* AuthUserDeleteResponse returned by {@link Auth#userDelete(ByteSequence)} contains a header.
* AuthUserDeleteResponse returned by {@link io.etcd.jetcd.Auth#userDelete(ByteSequence)} contains a header.
*/
public class AuthUserDeleteResponse extends AbstractResponse<io.etcd.jetcd.api.AuthUserDeleteResponse> {

View File

@ -18,12 +18,11 @@ package io.etcd.jetcd.auth;
import java.util.List;
import io.etcd.jetcd.Auth;
import io.etcd.jetcd.AbstractResponse;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.impl.AbstractResponse;
/**
* AuthUserGetResponse returned by {@link Auth#userGet(ByteSequence)} contains a header and
* AuthUserGetResponse returned by {@link io.etcd.jetcd.Auth#userGet(ByteSequence)} contains a header and
* a list of roles associated with the user.
*/
public class AuthUserGetResponse extends AbstractResponse<io.etcd.jetcd.api.AuthUserGetResponse> {
@ -34,8 +33,6 @@ public class AuthUserGetResponse extends AbstractResponse<io.etcd.jetcd.api.Auth
/**
* Returns a list of roles.
*
* @return the roles.
*/
public List<String> getRoles() {
return getResponse().getRolesList();

View File

@ -16,10 +16,12 @@
package io.etcd.jetcd.auth;
import io.etcd.jetcd.impl.AbstractResponse;
import io.etcd.jetcd.AbstractResponse;
/**
* AuthUserGrantRoleResponse
* AuthUserGrantRoleResponse returned by
* {@link io.etcd.jetcd.Auth#userGrantRole(io.etcd.jetcd.ByteSequence.ByteSequence,
* io.etcd.jetcd.ByteSequence.ByteSequence)} contains a header.
*/
public class AuthUserGrantRoleResponse extends AbstractResponse<io.etcd.jetcd.api.AuthUserGrantRoleResponse> {

View File

@ -18,11 +18,10 @@ package io.etcd.jetcd.auth;
import java.util.List;
import io.etcd.jetcd.Auth;
import io.etcd.jetcd.impl.AbstractResponse;
import io.etcd.jetcd.AbstractResponse;
/**
* AuthUserListResponse returned by {@link Auth#userList()} contains a header and
* AuthUserListResponse returned by {@link io.etcd.jetcd.Auth#userList()} contains a header and
* a list of users.
*/
public class AuthUserListResponse extends AbstractResponse<io.etcd.jetcd.api.AuthUserListResponse> {
@ -33,8 +32,6 @@ public class AuthUserListResponse extends AbstractResponse<io.etcd.jetcd.api.Aut
/**
* Returns a list of users.
*
* @return the users.
*/
public List<String> getUsers() {
return getResponse().getUsersList();

View File

@ -16,12 +16,11 @@
package io.etcd.jetcd.auth;
import io.etcd.jetcd.Auth;
import io.etcd.jetcd.AbstractResponse;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.impl.AbstractResponse;
/**
* AuthUserRevokeRoleResponse returned by {@link Auth#userRevokeRole(ByteSequence, ByteSequence)}
* AuthUserRevokeRoleResponse returned by {@link io.etcd.jetcd.Auth#userRevokeRole(ByteSequence, ByteSequence)}
* contains a header.
*/
public class AuthUserRevokeRoleResponse extends AbstractResponse<io.etcd.jetcd.api.AuthUserRevokeRoleResponse> {

View File

@ -39,8 +39,6 @@ public class Permission {
/**
* Returns the type of Permission: READ, WRITE, READWRITE, or UNRECOGNIZED.
*
* @return the permission type.
*/
public Type getPermType() {
return permType;

View File

@ -19,7 +19,7 @@ package io.etcd.jetcd.cluster;
import java.net.URI;
import java.util.List;
import io.etcd.jetcd.support.Util;
import io.etcd.jetcd.Util;
public class Member {
@ -31,8 +31,6 @@ public class Member {
/**
* Returns the member ID for this member.
*
* @return the id.
*/
public long getId() {
return member.getID();
@ -40,8 +38,6 @@ public class Member {
/**
* Returns the human-readable name of the member, ff the member is not started, the name will be an empty string.
*
* @return the name.
*/
public String getName() {
return member.getName();
@ -49,8 +45,6 @@ public class Member {
/**
* Returns the list of URLs the member exposes to the cluster for communication.
*
* @return the peer url
*/
public List<URI> getPeerURIs() {
return Util.toURIs(member.getPeerURLsList());
@ -59,19 +53,8 @@ public class Member {
/**
* Returns list of URLs the member exposes to clients for communication, if the member is not started, clientURLs will
* be empty.
*
* @return the client URIs.
*/
public List<URI> getClientURIs() {
return Util.toURIs(member.getClientURLsList());
}
/**
* Returns if the member is raft learner
*
* @return if the member is raft learner
*/
public boolean isLearner() {
return member.getIsLearner();
}
}

View File

@ -18,11 +18,10 @@ package io.etcd.jetcd.cluster;
import java.util.List;
import io.etcd.jetcd.Cluster;
import io.etcd.jetcd.impl.AbstractResponse;
import io.etcd.jetcd.AbstractResponse;
/**
* MemberAddResponse returned by {@link Cluster#addMember(List, boolean)}
* MemberAddResponse returned by {@link io.etcd.jetcd.Cluster#addMember(List)}
* contains a header, added member, and list of members after adding the new member.
*/
public class MemberAddResponse extends AbstractResponse<io.etcd.jetcd.api.MemberAddResponse> {

View File

@ -18,8 +18,8 @@ package io.etcd.jetcd.cluster;
import java.util.List;
import io.etcd.jetcd.AbstractResponse;
import io.etcd.jetcd.Cluster;
import io.etcd.jetcd.impl.AbstractResponse;
/**
* MemberListResponse returned by {@link Cluster#listMember()}

View File

@ -18,11 +18,10 @@ package io.etcd.jetcd.cluster;
import java.util.List;
import io.etcd.jetcd.Cluster;
import io.etcd.jetcd.impl.AbstractResponse;
import io.etcd.jetcd.AbstractResponse;
/**
* MemberRemoveResponse returned by {@link Cluster#removeMember(long)}
* MemberRemoveResponse returned by {@link io.etcd.jetcd.Cluster#removeMember(long)}
* contains a header and a list of member the removal of the member.
*/
public class MemberRemoveResponse extends AbstractResponse<io.etcd.jetcd.api.MemberRemoveResponse> {

View File

@ -18,11 +18,10 @@ package io.etcd.jetcd.cluster;
import java.util.List;
import io.etcd.jetcd.Cluster;
import io.etcd.jetcd.impl.AbstractResponse;
import io.etcd.jetcd.AbstractResponse;
/**
* MemberUpdateResponse returned by {@link Cluster#updateMember(long, List)}
* MemberUpdateResponse returned by {@link io.etcd.jetcd.Cluster#updateMember(long, List)}
* contains a header and a list of members after the member update.
*/
public class MemberUpdateResponse extends AbstractResponse<io.etcd.jetcd.api.MemberUpdateResponse> {

View File

@ -22,9 +22,7 @@ import java.util.stream.Collectors;
/**
* Util class for Cluster models.
*/
final class Util {
private Util() {
}
public class Util {
/**
* Converts a list of API member to a List of client side member.

View File

@ -16,26 +16,22 @@
package io.etcd.jetcd.election;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.impl.AbstractResponse;
import io.etcd.jetcd.AbstractResponse;
public class CampaignResponse extends AbstractResponse<io.etcd.jetcd.api.CampaignResponse> {
private final LeaderKey leaderKey;
public CampaignResponse(io.etcd.jetcd.api.CampaignResponse response) {
super(response, response.getHeader());
this.leaderKey = toLeaderKey(getResponse().getLeader());
}
this.leaderKey = new LeaderKey(
ByteSequence.from(getResponse().getLeader().getName()),
ByteSequence.from(getResponse().getLeader().getKey()),
getResponse().getLeader().getRev(),
getResponse().getLeader().getLease());
private static LeaderKey toLeaderKey(io.etcd.jetcd.api.LeaderKey leader) {
return new LeaderKey(leader.getName(), leader.getKey(), leader.getRev(), leader.getLease());
}
/**
* Returns the resources used for holding leadership of the election.
*
* @return the leader.
*/
public LeaderKey getLeader() {
return leaderKey;

View File

@ -16,15 +16,15 @@
package io.etcd.jetcd.election;
import io.etcd.jetcd.ByteSequence;
import com.google.protobuf.ByteString;
public class LeaderKey {
private final ByteSequence name;
private final ByteSequence key;
private final ByteString name;
private final ByteString key;
private final long revision;
private final long lease;
public LeaderKey(ByteSequence name, ByteSequence key, long revision, long lease) {
public LeaderKey(ByteString name, ByteString key, long revision, long lease) {
this.name = name;
this.key = key;
this.revision = revision;
@ -32,21 +32,17 @@ public class LeaderKey {
}
/**
* Returns the election identifier that corresponds to the leadership key.
*
* @return the name.
* Returns the election identifier that corresponds to the leadership key. *
*/
public ByteSequence getName() {
public ByteString getName() {
return name;
}
/**
* Returns the opaque key representing the ownership of the election. If the key
* is deleted, then leadership is lost.
*
* @return the key.
*/
public ByteSequence getKey() {
public ByteString getKey() {
return key;
}
@ -54,8 +50,6 @@ public class LeaderKey {
* Returns the creation revision of the key. It can be used to test for ownership
* of an election during transactions by testing the key's creation revision
* matches rev.
*
* @return the revision.
*/
public long getRevision() {
return revision;
@ -63,8 +57,6 @@ public class LeaderKey {
/**
* Returns the lease ID of the election leader.
*
* @return the lese id.
*/
public long getLease() {
return lease;

View File

@ -16,9 +16,9 @@
package io.etcd.jetcd.election;
import io.etcd.jetcd.AbstractResponse;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.KeyValue;
import io.etcd.jetcd.impl.AbstractResponse;
public class LeaderResponse extends AbstractResponse<io.etcd.jetcd.api.LeaderResponse> {
private final KeyValue kv;
@ -30,8 +30,6 @@ public class LeaderResponse extends AbstractResponse<io.etcd.jetcd.api.LeaderRes
/**
* Returns the key-value pair representing the latest leader update.
*
* @return the kv.
*/
public KeyValue getKv() {
return kv;

View File

@ -20,4 +20,5 @@ package io.etcd.jetcd.election;
* Signals that leader for given election does not exist.
*/
public class NoLeaderException extends RuntimeException {
public static final NoLeaderException INSTANCE = new NoLeaderException();
}

View File

@ -20,4 +20,5 @@ package io.etcd.jetcd.election;
* Signals that candidate is not a leader at the moment.
*/
public class NotLeaderException extends RuntimeException {
public static final NotLeaderException INSTANCE = new NotLeaderException();
}

View File

@ -16,7 +16,7 @@
package io.etcd.jetcd.election;
import io.etcd.jetcd.impl.AbstractResponse;
import io.etcd.jetcd.AbstractResponse;
public class ProclaimResponse extends AbstractResponse<io.etcd.jetcd.api.ProclaimResponse> {
public ProclaimResponse(io.etcd.jetcd.api.ProclaimResponse response) {

View File

@ -16,7 +16,7 @@
package io.etcd.jetcd.election;
import io.etcd.jetcd.impl.AbstractResponse;
import io.etcd.jetcd.AbstractResponse;
public class ResignResponse extends AbstractResponse<io.etcd.jetcd.api.ResignResponse> {
public ResignResponse(io.etcd.jetcd.api.ResignResponse response) {

View File

@ -1,113 +0,0 @@
/*
* Copyright 2016-2021 The jetcd 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.
*/
package io.etcd.jetcd.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.function.BiConsumer;
import io.etcd.jetcd.api.AuthenticateRequest;
import io.etcd.jetcd.api.VertxAuthGrpc;
import io.grpc.CallCredentials;
import io.grpc.ClientInterceptor;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.stub.MetadataUtils;
import com.google.protobuf.ByteString;
import static io.etcd.jetcd.Preconditions.checkArgument;
/**
* AuthTokenInterceptor fills header with Auth token of any rpc calls and
* refreshes token if the rpc results an invalid Auth token error.
*/
class AuthCredential extends CallCredentials {
public static final Metadata.Key<String> TOKEN = Metadata.Key.of("token", Metadata.ASCII_STRING_MARSHALLER);
private final ClientConnectionManager manager;
private volatile Metadata meta;
public AuthCredential(ClientConnectionManager manager) {
this.manager = manager;
}
@Override
public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) {
final Metadata meta = this.meta;
if (meta != null) {
applier.apply(meta);
} else {
authenticate(applier);
}
}
public void refresh() {
meta = null;
}
@SuppressWarnings("rawtypes")
private void authenticate(MetadataApplier applier) {
checkArgument(!manager.builder().user().isEmpty(), "username can not be empty.");
checkArgument(!manager.builder().password().isEmpty(), "password can not be empty.");
VertxAuthGrpc.AuthVertxStub authFutureStub = VertxAuthGrpc.newVertxStub(this.manager.getChannel());
List<ClientInterceptor> interceptorsChain = new ArrayList<>();
if (manager.builder().authHeaders() != null) {
Metadata metadata = new Metadata();
manager.builder().authHeaders().forEach((BiConsumer<Metadata.Key, Object>) metadata::put);
interceptorsChain.add(MetadataUtils.newAttachHeadersInterceptor(metadata));
}
if (manager.builder().authInterceptors() != null) {
interceptorsChain.addAll(manager.builder().authInterceptors());
}
if (!interceptorsChain.isEmpty()) {
authFutureStub = authFutureStub.withInterceptors(
interceptorsChain.toArray(new ClientInterceptor[0]));
}
final ByteString user = ByteString.copyFrom(this.manager.builder().user().getBytes());
final ByteString pass = ByteString.copyFrom(this.manager.builder().password().getBytes());
AuthenticateRequest request = AuthenticateRequest.newBuilder()
.setNameBytes(user)
.setPasswordBytes(pass)
.build();
try {
authFutureStub.authenticate(request)
.onFailure(t -> {
applier.fail(Status.UNAUTHENTICATED.withCause(t));
})
.onSuccess(h -> {
Metadata meta = new Metadata();
meta.put(TOKEN, h.getToken());
this.meta = meta;
applier.apply(this.meta);
});
} catch (Exception e) {
applier.fail(Status.UNAUTHENTICATED.withCause(e));
}
}
}

View File

@ -1,199 +0,0 @@
/*
* Copyright 2016-2021 The jetcd 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.
*/
package io.etcd.jetcd.impl;
import java.util.concurrent.CompletableFuture;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Election;
import io.etcd.jetcd.api.CampaignRequest;
import io.etcd.jetcd.api.LeaderRequest;
import io.etcd.jetcd.api.ProclaimRequest;
import io.etcd.jetcd.api.ResignRequest;
import io.etcd.jetcd.api.VertxElectionGrpc;
import io.etcd.jetcd.election.CampaignResponse;
import io.etcd.jetcd.election.LeaderKey;
import io.etcd.jetcd.election.LeaderResponse;
import io.etcd.jetcd.election.NoLeaderException;
import io.etcd.jetcd.election.NotLeaderException;
import io.etcd.jetcd.election.ProclaimResponse;
import io.etcd.jetcd.election.ResignResponse;
import io.etcd.jetcd.support.Errors;
import io.etcd.jetcd.support.Util;
import io.grpc.StatusRuntimeException;
import com.google.protobuf.ByteString;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.toEtcdException;
import static java.util.Objects.requireNonNull;
final class ElectionImpl extends Impl implements Election {
private final VertxElectionGrpc.ElectionVertxStub stub;
private final ByteSequence namespace;
ElectionImpl(ClientConnectionManager connectionManager) {
super(connectionManager);
this.stub = connectionManager.newStub(VertxElectionGrpc::newVertxStub);
this.namespace = connectionManager.getNamespace();
}
// Election operations are done in a context where a client is trying to implement
// some fault tolerance related use case; in that type of context, it makes sense to always
// apply require leader, since we don't want a client connected to a non-raft-leader server
// have an election method just go silent if the server the client happens to be connected to
// becomes partitioned from the actual raft-leader server in the etcd servers cluster:
// in that scenario and without required leader, an attempt to campaign could block forever
// not because some other client is already an election leader, but because the server the client
// is connected to is partitioned and can't tell.
// With require leader, in that case the call will fail and we give
// the client the ability to (a) know (b) retry on a different server.
// The retry on a different server should happen automatically if the connection manager is using
// a round robin strategy.
//
// Beware in the context of this election API, the word "leader" is overloaded.
// In the paragraph above when we say "raft-leader" we are talking about the etcd server that is a leader
// of the etcd servers cluster according to raft, we are not talking about the client that
// happens to be the leader of an election using the election API in this file.
private VertxElectionGrpc.ElectionVertxStub stubWithLeader() {
return Util.applyRequireLeader(true, stub);
}
@Override
public CompletableFuture<CampaignResponse> campaign(ByteSequence electionName, long leaseId, ByteSequence proposal) {
requireNonNull(electionName, "election name should not be null");
requireNonNull(proposal, "proposal should not be null");
CampaignRequest request = CampaignRequest.newBuilder()
.setName(Util.prefixNamespace(electionName, namespace))
.setValue(ByteString.copyFrom(proposal.getBytes()))
.setLease(leaseId)
.build();
return wrapConvertException(
execute(
() -> stubWithLeader().campaign(request),
CampaignResponse::new,
Errors::isRetryableForNoSafeRedoOp));
}
@Override
public CompletableFuture<ProclaimResponse> proclaim(LeaderKey leaderKey, ByteSequence proposal) {
requireNonNull(leaderKey, "leader key should not be null");
requireNonNull(proposal, "proposal should not be null");
ProclaimRequest request = ProclaimRequest.newBuilder()
.setLeader(
io.etcd.jetcd.api.LeaderKey.newBuilder()
.setKey(ByteString.copyFrom(leaderKey.getKey().getBytes()))
.setName(ByteString.copyFrom(leaderKey.getName().getBytes()))
.setLease(leaderKey.getLease())
.setRev(leaderKey.getRevision())
.build())
.setValue(ByteString.copyFrom(proposal.getBytes()))
.build();
return wrapConvertException(
execute(
() -> stubWithLeader().proclaim(request),
ProclaimResponse::new,
Errors::isRetryableForNoSafeRedoOp));
}
@Override
public CompletableFuture<LeaderResponse> leader(ByteSequence electionName) {
requireNonNull(electionName, "election name should not be null");
LeaderRequest request = LeaderRequest.newBuilder()
.setName(Util.prefixNamespace(electionName, namespace))
.build();
return wrapConvertException(
execute(
() -> stubWithLeader().leader(request),
response -> new LeaderResponse(response, namespace),
Errors::isRetryableForNoSafeRedoOp));
}
@Override
public void observe(ByteSequence electionName, Listener listener) {
requireNonNull(electionName, "election name should not be null");
requireNonNull(listener, "listener should not be null");
LeaderRequest request = LeaderRequest.newBuilder()
.setName(Util.prefixNamespace(electionName, namespace))
.build();
stubWithLeader().observeWithHandler(request,
value -> listener.onNext(new LeaderResponse(value, namespace)),
ignored -> listener.onCompleted(),
error -> listener.onError(toEtcdException(error)));
}
@Override
public CompletableFuture<ResignResponse> resign(LeaderKey leaderKey) {
requireNonNull(leaderKey, "leader key should not be null");
ResignRequest request = ResignRequest.newBuilder()
.setLeader(
io.etcd.jetcd.api.LeaderKey.newBuilder()
.setKey(ByteString.copyFrom(leaderKey.getKey().getBytes()))
.setName(ByteString.copyFrom(leaderKey.getName().getBytes()))
.setLease(leaderKey.getLease())
.setRev(leaderKey.getRevision())
.build())
.build();
return wrapConvertException(
execute(
() -> stubWithLeader().resign(request),
ResignResponse::new,
Errors::isRetryableForNoSafeRedoOp));
}
private <S> CompletableFuture<S> wrapConvertException(CompletableFuture<S> future) {
return future.exceptionally(e -> {
throw convertException(e);
});
}
private RuntimeException convertException(Throwable e) {
Throwable cause = e;
while (cause != null) {
if (cause instanceof StatusRuntimeException) {
StatusRuntimeException exception = (StatusRuntimeException) cause;
String description = exception.getStatus().getDescription();
// different APIs use different messages. we cannot distinguish missing leader error otherwise,
// because communicated status is always UNKNOWN
if ("election: not leader".equals(description)) {
// Candidate is not a leader at the moment.
// Note there is a one letter difference, but this exception type is not the same as
// NoLeaderException.
return new NotLeaderException();
}
if ("election: no leader".equals(description)) {
// Leader for given election does not exist.
// Note there is a one letter difference, but this exception type is not the same as
// NotLeaderException.
return new NoLeaderException();
}
}
cause = cause.getCause();
}
return toEtcdException(e);
}
}

View File

@ -1,160 +0,0 @@
package io.etcd.jetcd.impl;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.etcd.jetcd.common.exception.EtcdExceptionFactory;
import io.etcd.jetcd.support.Errors;
import io.grpc.Status;
import io.vertx.core.Future;
import dev.failsafe.Failsafe;
import dev.failsafe.RetryPolicy;
import dev.failsafe.RetryPolicyBuilder;
import static io.etcd.jetcd.support.Errors.isAuthStoreExpired;
import static io.etcd.jetcd.support.Errors.isInvalidTokenError;
abstract class Impl {
private final Logger logger;
private final ClientConnectionManager connectionManager;
protected Impl(ClientConnectionManager connectionManager) {
this.connectionManager = connectionManager;
this.logger = LoggerFactory.getLogger(getClass());
}
protected ClientConnectionManager connectionManager() {
return this.connectionManager;
}
protected Logger logger() {
return this.logger;
}
/**
* Converts Future of Type S to CompletableFuture of Type T.
*
* @param sourceFuture the Future to wrap
* @param resultConvert the result converter
* @return a {@link CompletableFuture} wrapping the given {@link Future}
*/
protected <S, T> CompletableFuture<T> completable(Future<S> sourceFuture, Function<S, T> resultConvert) {
return completable(sourceFuture, resultConvert, EtcdExceptionFactory::toEtcdException);
}
/**
* Converts Future of Type S to CompletableFuture of Type T.
*
* @param sourceFuture the Future to wrap
* @param resultConvert the result converter
* @param exceptionConverter the exception mapper
* @return a {@link CompletableFuture} wrapping the given {@link Future}
*/
protected <S, T> CompletableFuture<T> completable(
Future<S> sourceFuture,
Function<S, T> resultConvert,
Function<Throwable, Throwable> exceptionConverter) {
return completable(
sourceFuture.compose(
r -> Future.succeededFuture(resultConvert.apply(r)),
e -> Future.failedFuture(exceptionConverter.apply(e))));
}
/**
* Converts Future of Type S to CompletableFuture of Type T.
*
* @param sourceFuture the Future to wrap
* @return a {@link CompletableFuture} wrapping the given {@link Future}
*/
protected <S> CompletableFuture<S> completable(
Future<S> sourceFuture) {
return sourceFuture.toCompletionStage().toCompletableFuture();
}
/**
* execute the task and retry it in case of failure.
*
* @param supplier a function that returns a new Future.
* @param resultConvert a function that converts Type S to Type T.
* @param <S> Source type
* @param <T> Converted Type.
* @return a CompletableFuture with type T.
*/
protected <S, T> CompletableFuture<T> execute(
Supplier<Future<S>> supplier,
Function<S, T> resultConvert,
boolean autoRetry) {
return execute(supplier, resultConvert,
autoRetry ? Errors::isRetryableForSafeRedoOp : Errors::isRetryableForNoSafeRedoOp);
}
/**
* execute the task and retry it in case of failure.
*
* @param supplier a function that returns a new Future.
* @param resultConvert a function that converts Type S to Type T.
* @param doRetry a predicate to determine if a failure has to be retried
* @param <S> Source type
* @param <T> Converted Type.
* @return a CompletableFuture with type T.
*/
protected <S, T> CompletableFuture<T> execute(
Supplier<Future<S>> supplier,
Function<S, T> resultConvert,
Predicate<Status> doRetry) {
return Failsafe
.with(retryPolicy(doRetry))
.with(connectionManager.getExecutorService())
.getStageAsync(() -> supplier.get().toCompletionStage())
.thenApply(resultConvert);
}
protected <S> RetryPolicy<S> retryPolicy(Predicate<Status> doRetry) {
RetryPolicyBuilder<S> policy = RetryPolicy.<S> builder()
.onFailure(e -> {
logger.warn("retry failure (attempt: {}, error: {})",
e.getAttemptCount(),
e.getException() != null ? e.getException().getMessage() : "<none>");
})
.onRetry(e -> {
logger.debug("retry (attempt: {}, error: {})",
e.getAttemptCount(),
e.getLastException() != null ? e.getLastException().getMessage() : "<none>");
})
.onRetriesExceeded(e -> {
logger.warn("maximum number of auto retries reached (attempt: {}, error: {})",
e.getAttemptCount(),
e.getException() != null ? e.getException().getMessage() : "<none>");
})
.handleIf(throwable -> {
Status status = Status.fromThrowable(throwable);
if (isInvalidTokenError(status)) {
connectionManager.authCredential().refresh();
}
if (isAuthStoreExpired(status)) {
connectionManager.authCredential().refresh();
}
return doRetry.test(status);
})
.withMaxRetries(connectionManager.builder().retryMaxAttempts())
.withBackoff(
connectionManager.builder().retryDelay(),
connectionManager.builder().retryMaxDelay(),
connectionManager.builder().retryChronoUnit());
if (connectionManager.builder().retryMaxDuration() != null) {
policy = policy.withMaxDuration(connectionManager.builder().retryMaxDuration());
}
return policy.build();
}
}

Some files were not shown because too many files have changed in this diff Show More