Skip to content
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

Support dynamic properties in HTTP announcement #1157

Merged
merged 4 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Fail decoding with JsonCodec when content is not single JSON value.
Previously the codec decoded first JSON value only, ignoring the rest
of the payload.
- Support Guice providers in HTTP announcement custom properties.
- Allow only `@DefunctConfig` class without `@Config` annotation
- Update airbase to 156
- Update bouncycastle to 1.78.1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,28 @@
*/
package io.airlift.discovery.client;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.inject.Binder;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Provider;
import com.google.inject.Scopes;
import com.google.inject.TypeLiteral;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.multibindings.Multibinder;
import io.airlift.discovery.client.ServiceAnnouncement.ServiceAnnouncementBuilder;

import java.lang.annotation.Annotation;
import java.util.Map;

import static com.google.inject.multibindings.MapBinder.newMapBinder;
import static com.google.inject.multibindings.Multibinder.newSetBinder;
import static io.airlift.configuration.ConfigBinder.configBinder;
import static io.airlift.discovery.client.ServiceAnnouncement.serviceAnnouncement;
import static io.airlift.discovery.client.ServiceTypes.serviceType;
import static java.util.Objects.requireNonNull;
import static java.util.UUID.randomUUID;

public class DiscoveryBinder
{
Expand Down Expand Up @@ -84,11 +93,12 @@ public <T extends ServiceAnnouncement> void bindServiceAnnouncement(Class<? exte
serviceAnnouncementBinder.addBinding().toProvider(announcementProviderClass);
}

public ServiceAnnouncementBuilder bindHttpAnnouncement(String type)
public HttpAnnouncementBindingBuilder bindHttpAnnouncement(String type)
{
ServiceAnnouncementBuilder serviceAnnouncementBuilder = serviceAnnouncement(type);
bindServiceAnnouncement(new HttpAnnouncementProvider(serviceAnnouncementBuilder));
return serviceAnnouncementBuilder;
HttpAnnouncement annotation = new HttpAnnouncementImpl(type + "." + randomUUID());
MapBinder<String, String> propertiesBinder = newMapBinder(binder, String.class, String.class, annotation);
bindServiceAnnouncement(new HttpAnnouncementProvider(type, annotation));
return new HttpAnnouncementBindingBuilder(propertiesBinder);
}

public void bindHttpSelector(String type)
Expand All @@ -104,15 +114,74 @@ public void bindHttpSelector(ServiceType serviceType)
binder.bind(HttpServiceSelector.class).annotatedWith(serviceType).toProvider(new HttpServiceSelectorProvider(serviceType.value())).in(Scopes.SINGLETON);
}

public static class HttpAnnouncementBindingBuilder
{
private final MapBinder<String, String> propertiesBinder;

public HttpAnnouncementBindingBuilder(MapBinder<String, String> propertiesBinder)
{
this.propertiesBinder = requireNonNull(propertiesBinder, "propertiesBinder is null");
}

@CanIgnoreReturnValue
public HttpAnnouncementBindingBuilder addProperty(String key, String value)
{
requireNonNull(key, "key is null");
requireNonNull(value, "value is null");
propertiesBinder.addBinding(key).toInstance(value);
return this;
}

@CanIgnoreReturnValue
public HttpAnnouncementBindingBuilder addProperties(Map<String, String> properties)
{
properties.forEach(this::addProperty);
return this;
}

@CanIgnoreReturnValue
public HttpAnnouncementBindingBuilder bindPropertyProvider(String key, Provider<String> provider)
{
requireNonNull(key, "key is null");
requireNonNull(provider, "provider is null");
propertiesBinder.addBinding(key).toProvider(provider);
return this;
}

@CanIgnoreReturnValue
public HttpAnnouncementBindingBuilder bindPropertyProvider(String key, Class<? extends Provider<String>> providerType)
{
return bindPropertyProvider(key, Key.get(providerType));
}

@CanIgnoreReturnValue
public HttpAnnouncementBindingBuilder bindPropertyProvider(String key, Key<? extends Provider<String>> providerKey)
{
requireNonNull(key, "key is null");
requireNonNull(providerKey, "providerKey is null");
propertiesBinder.addBinding(key).toProvider(providerKey);
return this;
}
}

static class HttpAnnouncementProvider
implements Provider<ServiceAnnouncement>
{
private final ServiceAnnouncementBuilder builder;
private final String type;
private final Annotation annotation;
private Injector injector;
private AnnouncementHttpServerInfo httpServerInfo;

public HttpAnnouncementProvider(ServiceAnnouncementBuilder serviceAnnouncementBuilder)
public HttpAnnouncementProvider(String type, Annotation annotation)
findepi marked this conversation as resolved.
Show resolved Hide resolved
{
this.type = type;
this.annotation = annotation;
}

@Inject
public void setInjector(Injector injector)
{
builder = serviceAnnouncementBuilder;
this.injector = injector;
}

@Inject
Expand All @@ -124,6 +193,9 @@ public void setAnnouncementHttpServerInfo(AnnouncementHttpServerInfo httpServerI
@Override
public ServiceAnnouncement get()
{
ServiceAnnouncementBuilder builder = serviceAnnouncement(type);
builder.addProperties(injector.getInstance(Key.get(new TypeLiteral<Map<String, String>>() {}, annotation)));

if (httpServerInfo.getHttpUri() != null) {
builder.addProperty("http", httpServerInfo.getHttpUri().toString());
builder.addProperty("http-external", httpServerInfo.getHttpExternalUri().toString());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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.airlift.discovery.client;

import com.google.inject.BindingAnnotation;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Retention(RUNTIME)
@Target({FIELD, PARAMETER, METHOD})
@BindingAnnotation
@interface HttpAnnouncement
{
String announcementId();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop this header - not required.

* Copyright 2010 Proofpoint, Inc.
*
* 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.airlift.discovery.client;

import java.lang.annotation.Annotation;

import static java.lang.String.format;
import static java.util.Objects.requireNonNull;

class HttpAnnouncementImpl
implements HttpAnnouncement
{
private final String announcementId;

public HttpAnnouncementImpl(String announcementId)
{
this.announcementId = requireNonNull(announcementId, "announcementId is null");
}

public String announcementId()
{
return announcementId;
}

public String toString()
{
return format("@%s(announcementId=\"%s\")", annotationType().getName(), announcementId.replace("\"", "\\\""));
}

@Override
public boolean equals(Object o)
{
if (!(o instanceof HttpAnnouncement that)) {
return false;
}
return announcementId.equals(that.announcementId());
}

@Override
public int hashCode()
{
// see Annotation.hashCode()
int result = 0;
result += ((127 * "announcementId".hashCode()) ^ announcementId.hashCode());
return result;
}

public Class<? extends Annotation> annotationType()
{
return HttpAnnouncement.class;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@
package io.airlift.discovery.client;

import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import io.airlift.discovery.client.testing.TestingDiscoveryModule;
import org.testng.annotations.Test;

import java.net.URI;
import java.util.Set;
import java.util.UUID;

import static com.google.common.collect.MoreCollectors.onlyElement;
import static io.airlift.discovery.client.DiscoveryBinder.discoveryBinder;
Expand Down Expand Up @@ -142,6 +145,38 @@ public void testHttpAnnouncementWithCustomProperties()
assertAnnouncement(announcements, announcement);
}

@Test
public void testHttpAnnouncementWithCustomProvidedProperties()
{
StaticAnnouncementHttpServerInfoImpl httpServerInfo = new StaticAnnouncementHttpServerInfoImpl(
URI.create("http://127.0.0.1:4444"),
URI.create("http://example.com:4444"),
URI.create("https://127.0.0.1:4444"),
URI.create("https://example.com:4444"));
String randomValue = UUID.randomUUID().toString();

Injector injector = Guice.createInjector(
new TestingDiscoveryModule(),
binder -> {
binder.bind(AnnouncementHttpServerInfo.class).toInstance(httpServerInfo);
discoveryBinder(binder).bindHttpAnnouncement("apple")
.addProperty("instance-property", "my-instance")
.bindPropertyProvider("provided-by-instance", () -> "provided-constant: " + randomValue)
.bindPropertyProvider("provided-by-injected", StringPropertyProvider.class);
});

Set<ServiceAnnouncement> announcements = injector.getInstance(new Key<>() {});
assertAnnouncement(announcements, serviceAnnouncement("apple")
.addProperty("instance-property", "my-instance")
.addProperty("provided-by-instance", "provided-constant: " + randomValue)
.addProperty("provided-by-injected", "concatenated: http://127.0.0.1:4444 https://127.0.0.1:4444")
.addProperty("http", "http://127.0.0.1:4444")
.addProperty("http-external", "http://example.com:4444")
.addProperty("https", "https://127.0.0.1:4444")
.addProperty("https-external", "https://example.com:4444")
.build());
}

private void assertAnnouncement(Set<ServiceAnnouncement> actualAnnouncements, ServiceAnnouncement expected)
{
assertNotNull(actualAnnouncements);
Expand All @@ -150,4 +185,22 @@ private void assertAnnouncement(Set<ServiceAnnouncement> actualAnnouncements, Se
assertEquals(announcement.getType(), expected.getType());
assertEquals(announcement.getProperties(), expected.getProperties());
}

public static class StringPropertyProvider
implements Provider<String>
{
private final AnnouncementHttpServerInfo httpServerInfo;

@Inject
public StringPropertyProvider(AnnouncementHttpServerInfo httpServerInfo)
{
this.httpServerInfo = httpServerInfo;
}

@Override
public String get()
{
return "concatenated: %s %s".formatted(httpServerInfo.getHttpUri(), httpServerInfo.getHttpsUri());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2010 Proofpoint, Inc.
*
* 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.airlift.discovery.client;

import org.testng.annotations.Test;

import static io.airlift.testing.EquivalenceTester.equivalenceTester;
import static org.testng.Assert.assertEquals;

public class TestHttpAnnouncementImpl
{
@HttpAnnouncement(announcementId = "apple")
private final HttpAnnouncement appleHttpAnnouncement;

@HttpAnnouncement(announcementId = "banana")
private final HttpAnnouncement bananaHttpAnnouncement;

@HttpAnnouncement(announcementId = "quot\"ation-and-\\backslash")
private final HttpAnnouncement httpAnnouncementWithCharacters;

public TestHttpAnnouncementImpl()
{
try {
this.appleHttpAnnouncement = getClass().getDeclaredField("appleHttpAnnouncement").getAnnotation(HttpAnnouncement.class);
this.bananaHttpAnnouncement = getClass().getDeclaredField("bananaHttpAnnouncement").getAnnotation(HttpAnnouncement.class);
this.httpAnnouncementWithCharacters = getClass().getDeclaredField("httpAnnouncementWithCharacters").getAnnotation(HttpAnnouncement.class);
}
catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}

@Test
public void testAnnouncementId()
{
assertEquals(new HttpAnnouncementImpl("type A").announcementId(), "type A");
}

@Test
public void testAnnotationType()
{
assertEquals(new HttpAnnouncementImpl("apple").annotationType(), HttpAnnouncement.class);
assertEquals(new HttpAnnouncementImpl("apple").annotationType(), appleHttpAnnouncement.annotationType());
}

@Test
public void testEquivalence()
{
equivalenceTester()
.addEquivalentGroup(appleHttpAnnouncement, new HttpAnnouncementImpl("apple"))
.addEquivalentGroup(bananaHttpAnnouncement, new HttpAnnouncementImpl("banana"))
.check();
}
}