-
Notifications
You must be signed in to change notification settings - Fork 0
Initial push #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Initial push #2
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
language: java | ||
jdk: | ||
- oraclejdk8 | ||
script: | ||
- "./gradlew build" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
group 'io.datanerds' | ||
version '1.0-SNAPSHOT' | ||
|
||
def grpcVersion = '1.1.2' | ||
def consulClientVersion = '0.11.3' | ||
def slf4jVersion = '1.7.21' | ||
|
||
buildscript { | ||
repositories { | ||
mavenCentral() | ||
} | ||
dependencies { | ||
classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.1" | ||
} | ||
} | ||
|
||
subprojects { | ||
apply plugin: 'java' | ||
apply plugin: 'com.google.protobuf' | ||
|
||
repositories { | ||
mavenCentral() | ||
mavenLocal() | ||
} | ||
|
||
dependencies { | ||
compile "io.grpc:grpc-all:${grpcVersion}" | ||
compile "io.grpc:grpc-core:${grpcVersion}" | ||
compile "io.grpc:grpc-netty:${grpcVersion}" | ||
compile "io.grpc:grpc-protobuf:${grpcVersion}" | ||
compile "io.grpc:grpc-stub:${grpcVersion}" | ||
compile "com.orbitz.consul:consul-client:${consulClientVersion}" | ||
compile "org.slf4j:slf4j-api:${slf4jVersion}" | ||
|
||
testCompile "junit:junit:4.12" | ||
testCompile 'org.hamcrest:hamcrest-library:1.3' | ||
testCompile 'org.awaitility:awaitility:2.0.0' | ||
testCompile 'com.pszymczyk.consul:embedded-consul:0.2.3' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting! 👍 |
||
} | ||
|
||
protobuf { | ||
protoc { | ||
artifact = 'com.google.protobuf:protoc:3.2.0' | ||
} | ||
plugins { | ||
grpc { | ||
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" | ||
} | ||
} | ||
generateProtoTasks { | ||
all()*.plugins { | ||
grpc { | ||
option 'enable_deprecated=false' | ||
} | ||
} | ||
} | ||
} | ||
|
||
sourceSets { | ||
main { | ||
proto { | ||
srcDirs += file("${projectDir}/build/generated/source/proto/main/java"); | ||
srcDirs += file("${projectDir}/build/generated/source/proto/main/grpc"); | ||
srcDirs += file("${projectDir}/build/generated/source/proto/test/java"); | ||
srcDirs += file("${projectDir}/build/generated/source/proto/test/grpc"); | ||
} | ||
} | ||
} | ||
} | ||
|
||
project(":consulnameresolver") { | ||
task javadocJar(type: Jar) { | ||
classifier = 'javadoc' | ||
from javadoc | ||
} | ||
|
||
task sourcesJar(type: Jar) { | ||
classifier = 'sources' | ||
from sourceSets.main.allSource | ||
} | ||
|
||
artifacts { | ||
archives javadocJar, sourcesJar | ||
} | ||
} | ||
|
||
buildscript { | ||
repositories { | ||
mavenCentral() | ||
} | ||
dependencies { | ||
classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.5.3" | ||
} | ||
} | ||
|
||
|
||
apply plugin: 'io.codearte.nexus-staging' | ||
|
||
if (project.hasProperty('staging')) { | ||
|
||
nexusStaging { | ||
username = ossrhUser | ||
password = ossrhPassword | ||
} | ||
|
||
project(":consulnameresolver") { | ||
apply plugin: 'signing' | ||
apply plugin: 'maven' | ||
apply plugin: 'osgi' | ||
|
||
jar { | ||
manifest { | ||
version = project.version | ||
symbolicName = "$project.group.$baseName" | ||
} | ||
} | ||
|
||
signing { | ||
sign configurations.archives | ||
} | ||
|
||
nexusStaging { | ||
packageGroup = "io.datanerds" | ||
stagingProfileId = "36a7a00c49b56" | ||
} | ||
|
||
uploadArchives { | ||
repositories { | ||
mavenDeployer { | ||
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } | ||
repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { | ||
authentication(userName: ossrhUser, password: ossrhPassword) | ||
} | ||
pom.project { | ||
name project.name | ||
groupId 'io.datanerds' | ||
description project.description | ||
packaging 'jar' | ||
url 'https://github.com/datanerds-io/ConsulNameResolver' | ||
|
||
scm { | ||
connection 'scm:git:https://github.com/datanerds-io/ConsulNameResolver.git' | ||
developerConnection 'scm:git:git@github.com:datanerds-io/ConsulNameResolver.git' | ||
url 'https://github.com/datanerds-io/ConsulNameResolver.git' | ||
} | ||
|
||
licenses { | ||
license { | ||
name 'Eclipse Public License 1.0' | ||
url 'https://opensource.org/licenses/EPL-1.0' | ||
distribution 'repo' | ||
} | ||
} | ||
|
||
developers { | ||
developer { | ||
id = 'utobi' | ||
name = 'Tobias Ullrich' | ||
email = 'github-2017@ullrich.io' | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package io.datanerds.grpc; | ||
|
||
import com.google.common.base.Preconditions; | ||
import com.google.common.net.HostAndPort; | ||
import com.orbitz.consul.Consul; | ||
import com.orbitz.consul.HealthClient; | ||
import com.orbitz.consul.cache.ServiceHealthCache; | ||
import com.orbitz.consul.cache.ServiceHealthKey; | ||
import com.orbitz.consul.model.ConsulResponse; | ||
import com.orbitz.consul.model.health.ServiceHealth; | ||
import com.orbitz.consul.option.ImmutableCatalogOptions; | ||
import io.grpc.*; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.net.InetSocketAddress; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.stream.Collectors; | ||
|
||
public class ConsulNameResolver extends NameResolver { | ||
public static final String DEFAULT_ADDRESS = "localhost"; | ||
public static final Integer DEFAULT_PORT = 8383; | ||
private static final Logger logger = LoggerFactory.getLogger(ConsulNameResolver.class); | ||
private final String authority; | ||
private final String service; | ||
private Listener listener; | ||
private Consul consul; | ||
private HealthClient healthClient; | ||
private Optional<String> datacenter; | ||
private Optional<List<String>> tags; | ||
|
||
public ConsulNameResolver(ConsulQueryParameter parameter) { | ||
this.authority = parameter.consulAddress.orElse( | ||
String.format("%s:%s", DEFAULT_ADDRESS, DEFAULT_PORT)); //todo move to URI | ||
this.service = parameter.service; | ||
this.datacenter = parameter.datacenter; | ||
this.tags = parameter.tags; | ||
|
||
HostAndPort consulHostAndPort = HostAndPort.fromString(this.authority); | ||
this.consul = Consul.builder().withHostAndPort(consulHostAndPort).build(); | ||
this.healthClient = this.consul.healthClient(); | ||
} | ||
|
||
@Override | ||
public String getServiceAuthority() { | ||
return this.authority; | ||
} | ||
|
||
@Override | ||
public void shutdown() { | ||
} | ||
|
||
@Override | ||
public void start(Listener listener) { | ||
Preconditions.checkState(this.listener == null, "already started"); | ||
this.listener = Preconditions.checkNotNull(listener, "listener"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. error message "listener"? Should probably be more informative: "listener cannot be empty". |
||
logger.debug("Resolving service '{}' using consul at '%{}'", this.service, this.authority); | ||
|
||
ServiceHealthCache svHealth = | ||
ServiceHealthCache.newCache(this.healthClient, this.service, true, buildCatalogOptions(), 5); | ||
|
||
svHealth.addListener((Map<ServiceHealthKey, ServiceHealth> newValues) -> { | ||
if (newValues.isEmpty()) { | ||
updateListenerWithEmptyResult(); | ||
return; | ||
} | ||
listener.onUpdate(generateResolvedServerList(newValues), Attributes.EMPTY); | ||
}); | ||
|
||
try { | ||
svHealth.start(); | ||
runInitialResolve(); | ||
} catch (Exception ex) { | ||
throw new RuntimeException("Exception while trying to start consul client for name resolution", ex); | ||
} | ||
} | ||
|
||
private ImmutableCatalogOptions buildCatalogOptions() { | ||
ImmutableCatalogOptions.Builder options = ImmutableCatalogOptions.builder(); | ||
|
||
if (this.datacenter.isPresent()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For conciseness I'd use
but that's just matter of taste though. |
||
options.datacenter(com.google.common.base.Optional.of(this.datacenter.get())); | ||
} | ||
|
||
if (this.tags.isPresent()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
this.tags.get().forEach(options::tag); | ||
} | ||
return options.build(); | ||
} | ||
|
||
private void updateListenerWithEmptyResult() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably rename method to something like |
||
logger.warn("No servers could be resolved for '{}'", ConsulNameResolver.this.service); | ||
this.listener.onError(Status.UNAVAILABLE.augmentDescription(String.format( | ||
"No servers could be resolved for service '%s' from authority '%s'", | ||
ConsulNameResolver.this.service, ConsulNameResolver.this.authority))); | ||
this.listener.onUpdate(new ArrayList<>(), Attributes.EMPTY); | ||
} | ||
|
||
private List<ResolvedServerInfoGroup> generateResolvedServerList(Map<ServiceHealthKey, ServiceHealth> newValues) { | ||
return newValues | ||
.keySet() | ||
.stream() | ||
.map(key -> new InetSocketAddress(key.getHost(), key.getPort())) | ||
.map(socketAddress -> ResolvedServerInfoGroup.builder() | ||
.add(new ResolvedServerInfo(socketAddress, Attributes.EMPTY))) | ||
.map(ResolvedServerInfoGroup.Builder::build).collect(Collectors.toList()); | ||
} | ||
|
||
private void runInitialResolve() { | ||
ConsulResponse<List<ServiceHealth>> healthyServiceInstances = | ||
this.healthClient.getHealthyServiceInstances(this.service, buildCatalogOptions()); | ||
if (healthyServiceInstances.getResponse().isEmpty()) { | ||
this.listener.onError(Status.UNAVAILABLE.augmentDescription(String.format( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicate code (see L97) |
||
"No servers could be resolved for service '%s' from authority '%s'", | ||
this.service, this.authority))); | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package io.datanerds.grpc; | ||
|
||
import com.google.common.base.Preconditions; | ||
import io.grpc.Attributes; | ||
import io.grpc.NameResolver; | ||
import io.grpc.NameResolverProvider; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.net.URI; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
public class ConsulNameResolverProvider extends NameResolverProvider { | ||
private static final Logger logger = LoggerFactory.getLogger(ConsulNameResolverProvider.class); | ||
|
||
private static final String SCHEME = "consul"; | ||
private static final String QUERY_PARAMETER_TAGS = "tag"; | ||
private static final String QUERY_PARAMETER_DATACENTER = "dc"; | ||
|
||
@Override | ||
protected boolean isAvailable() { | ||
return true; | ||
} | ||
|
||
@Override | ||
protected int priority() { | ||
return 5; | ||
} | ||
|
||
@Override | ||
public String getDefaultScheme() { | ||
return SCHEME; | ||
} | ||
|
||
@Override | ||
public NameResolver newNameResolver(URI targetUri, Attributes params) { | ||
//Contract is to return null if resolver is not able to handle uri | ||
try { | ||
return new ConsulNameResolver(validateInputURI(targetUri)); | ||
} catch (IllegalArgumentException | NullPointerException ex) { | ||
return null; | ||
} | ||
} | ||
|
||
private void validateScheme(URI uri) { | ||
if (!SCHEME.equalsIgnoreCase(uri.getScheme())) { | ||
throw new IllegalArgumentException("scheme for uri is not 'consul'"); | ||
} | ||
} | ||
|
||
protected ConsulQueryParameter validateInputURI(final URI uri) { | ||
validateScheme(uri); | ||
Preconditions.checkNotNull(uri.getHost(), "Host cannot be empty"); | ||
Preconditions.checkNotNull(uri.getPath(), "Path cannot be empty"); | ||
Preconditions.checkArgument(uri.getPath().startsWith("/"), | ||
"the path component (%s) of the target (%s) must start with '/'", uri.getPath(), uri); | ||
|
||
Map<String, List<String>> splitQuery = URIUtils.splitQuery(uri); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unused! |
||
Map<String, List<String>> queryParameters = URIUtils.splitQuery(uri); | ||
Preconditions.checkArgument(!(queryParameters.containsKey(QUERY_PARAMETER_DATACENTER) | ||
&& queryParameters.get(QUERY_PARAMETER_DATACENTER).size() != 1), | ||
"Only one datacenter can be defined"); | ||
|
||
String consul; | ||
if (uri.getPort() == -1) { | ||
consul = String.format("%s:%s", uri.getHost(), ConsulNameResolver.DEFAULT_PORT); | ||
} else { | ||
consul = String.format("%s:%s", uri.getHost(), uri.getPort()); | ||
} | ||
|
||
String service = uri.getPath().substring(1); | ||
|
||
ConsulQueryParameter.Builder builder = new ConsulQueryParameter.Builder(service) | ||
.withConsulAddress(consul); | ||
|
||
if (queryParameters.containsKey(QUERY_PARAMETER_DATACENTER)) { | ||
builder.withDatacenter(queryParameters.get(QUERY_PARAMETER_DATACENTER).get(0)); | ||
} | ||
if (queryParameters.containsKey(QUERY_PARAMETER_TAGS)) { | ||
builder.withTags(queryParameters.get(QUERY_PARAMETER_TAGS)); | ||
} | ||
|
||
return builder.build(); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I read at a glance here, you need either
grpc-all
or the other deps?