diff --git a/components/proxy/pom.xml b/components/proxy/pom.xml index 05709c29c9..f515170cc1 100644 --- a/components/proxy/pom.xml +++ b/components/proxy/pom.xml @@ -92,6 +92,11 @@ jackson-dataformat-yaml + + com.fasterxml.jackson.module + jackson-module-kotlin + + logback-classic ch.qos.logback diff --git a/components/proxy/src/main/java/com/hotels/styx/infrastructure/configuration/yaml/JsonNodeConfig.java b/components/proxy/src/main/java/com/hotels/styx/infrastructure/configuration/yaml/JsonNodeConfig.java index a54b306cdb..81e59987b1 100644 --- a/components/proxy/src/main/java/com/hotels/styx/infrastructure/configuration/yaml/JsonNodeConfig.java +++ b/components/proxy/src/main/java/com/hotels/styx/infrastructure/configuration/yaml/JsonNodeConfig.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.module.kotlin.KotlinModule; import com.hotels.styx.api.configuration.ConversionException; import com.hotels.styx.api.configuration.Configuration; @@ -38,7 +39,9 @@ * jackson API and Styx API. */ public class JsonNodeConfig implements Configuration { - static final ObjectMapper YAML_MAPPER = addStyxMixins(new ObjectMapper(new YAMLFactory())) + static final ObjectMapper YAML_MAPPER = addStyxMixins( + new ObjectMapper(new YAMLFactory()) + .registerModule(new KotlinModule())) .configure(FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(AUTO_CLOSE_SOURCE, true); @@ -58,7 +61,7 @@ public JsonNodeConfig(JsonNode rootNode) { * Construct an instance from a JSON node. * * @param rootNode a JSON node - * @param mapper mapper to convert JSON into objects + * @param mapper mapper to convert JSON into objects */ protected JsonNodeConfig(JsonNode rootNode, ObjectMapper mapper) { this.rootNode = requireNonNull(rootNode); diff --git a/components/proxy/src/main/java/com/hotels/styx/routing/config/RoutingObjectFactory.java b/components/proxy/src/main/java/com/hotels/styx/routing/config/RoutingObjectFactory.java index 51b547d788..f6bb235e22 100644 --- a/components/proxy/src/main/java/com/hotels/styx/routing/config/RoutingObjectFactory.java +++ b/components/proxy/src/main/java/com/hotels/styx/routing/config/RoutingObjectFactory.java @@ -82,7 +82,7 @@ public class RoutingObjectFactory { .put(PROXY_TO_BACKEND, ProxyToBackend.SCHEMA) .put(PATH_PREFIX_ROUTER, PathPrefixRouter.SCHEMA) .put(HOST_PROXY, HostProxy.SCHEMA) - .put(LOAD_BALANCING_GROUP, LoadBalancingGroup.SCHEMA) + .put(LOAD_BALANCING_GROUP, LoadBalancingGroup.Companion.getSCHEMA()) .build(); } diff --git a/components/proxy/src/main/java/com/hotels/styx/routing/handlers/LoadBalancingGroup.java b/components/proxy/src/main/java/com/hotels/styx/routing/handlers/LoadBalancingGroup.java deleted file mode 100644 index 7477ebeae4..0000000000 --- a/components/proxy/src/main/java/com/hotels/styx/routing/handlers/LoadBalancingGroup.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - Copyright (C) 2013-2019 Expedia 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 com.hotels.styx.routing.handlers; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableSet; -import com.hotels.styx.api.Eventual; -import com.hotels.styx.api.HttpInterceptor; -import com.hotels.styx.api.Id; -import com.hotels.styx.api.LiveHttpRequest; -import com.hotels.styx.api.LiveHttpResponse; -import com.hotels.styx.api.configuration.ObjectStore; -import com.hotels.styx.api.extension.ActiveOrigins; -import com.hotels.styx.api.extension.RemoteHost; -import com.hotels.styx.api.extension.loadbalancing.spi.LoadBalancer; -import com.hotels.styx.api.extension.service.StickySessionConfig; -import com.hotels.styx.client.OriginRestrictionLoadBalancingStrategy; -import com.hotels.styx.client.StyxBackendServiceClient; -import com.hotels.styx.client.loadbalancing.strategies.PowerOfTwoStrategy; -import com.hotels.styx.client.stickysession.StickySessionLoadBalancingStrategy; -import com.hotels.styx.config.schema.Schema; -import com.hotels.styx.infrastructure.configuration.yaml.JsonNodeConfig; -import com.hotels.styx.routing.RoutingObject; -import com.hotels.styx.routing.RoutingObjectAdapter; -import com.hotels.styx.routing.RoutingObjectRecord; -import com.hotels.styx.routing.config.HttpHandlerFactory; -import com.hotels.styx.routing.config.RoutingObjectDefinition; -import com.hotels.styx.routing.db.StyxObjectStore; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.Disposable; -import reactor.core.publisher.Flux; - -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -import static com.hotels.styx.api.extension.Origin.newOriginBuilder; -import static com.hotels.styx.api.extension.RemoteHost.remoteHost; -import static com.hotels.styx.api.extension.service.StickySessionConfig.stickySessionDisabled; -import static com.hotels.styx.config.schema.SchemaDsl.bool; -import static com.hotels.styx.config.schema.SchemaDsl.field; -import static com.hotels.styx.config.schema.SchemaDsl.integer; -import static com.hotels.styx.config.schema.SchemaDsl.object; -import static com.hotels.styx.config.schema.SchemaDsl.optional; -import static com.hotels.styx.config.schema.SchemaDsl.string; -import static com.hotels.styx.routing.config.RoutingSupport.missingAttributeError; -import static java.lang.String.join; -import static java.util.Objects.requireNonNull; -import static java.util.concurrent.CompletableFuture.completedFuture; - -/** - * Add an API for LoadBalancingGroup class. - */ -public class LoadBalancingGroup implements RoutingObject { - public static final Schema.FieldType SCHEMA = object( - field("origins", string()), - optional("originsRestrictionCookie", string()), - optional("stickySession", object( - field("enabled", bool()), - field("timeoutSeconds", integer()) - )) - ); - - private static final Logger LOGGER = LoggerFactory.getLogger(LoadBalancingGroup.class); - - private final StyxBackendServiceClient client; - private final Disposable changeWatcher; - - - @VisibleForTesting - LoadBalancingGroup(StyxBackendServiceClient client, Disposable changeWatcher) { - this.client = requireNonNull(client); - this.changeWatcher = requireNonNull(changeWatcher); - } - - @Override - public Eventual handle(LiveHttpRequest request, HttpInterceptor.Context context) { - return new Eventual<>(client.sendRequest(request)); - } - - @Override - public CompletableFuture stop() { - changeWatcher.dispose(); - return completedFuture(null); - } - - /** - * Add an API doc for Factory class. - */ - public static class Factory implements HttpHandlerFactory { - - @Override - public RoutingObject build(List parents, Context context, RoutingObjectDefinition configBlock) { - JsonNodeConfig config = new JsonNodeConfig(configBlock.config()); - String name = parents.get(parents.size() - 1); - - String appId = config.get("origins") - .orElseThrow(() -> missingAttributeError(configBlock, join(".", parents), "origins")); - - StickySessionConfig stickySessionConfig = config.get("stickySession", StickySessionConfig.class) - .orElse(stickySessionDisabled()); - - String originsRestrictionCookie = config.get("originsRestrictionCookie") - .orElse(null); - - StyxObjectStore routeDb = context.routeDb(); - AtomicReference> remoteHosts = new AtomicReference<>(ImmutableSet.of()); - - Disposable watch = Flux.from(routeDb.watch()).subscribe( - snapshot -> routeDatabaseChanged(appId, snapshot, remoteHosts), - cause -> watchFailed(name, cause), - () -> watchCompleted(name) - ); - - LoadBalancer loadBalancer = loadBalancer(stickySessionConfig, originsRestrictionCookie, remoteHosts::get); - StyxBackendServiceClient client = new StyxBackendServiceClient.Builder(Id.id(name)) - .loadBalancer(loadBalancer) - .metricsRegistry(context.environment().metricRegistry()) - .originIdHeader(context.environment().configuration().styxHeaderConfig().originIdHeaderName()) - .stickySessionConfig(stickySessionConfig) - .originsRestrictionCookieName(originsRestrictionCookie) - .build(); - return new LoadBalancingGroup(client, watch); - } - - private static LoadBalancer loadBalancer(StickySessionConfig stickySessionConfig, String originsRestrictionCookie, ActiveOrigins activeOrigins) { - LoadBalancer loadBalancer = new PowerOfTwoStrategy(activeOrigins); - if (stickySessionConfig.stickySessionEnabled()) { - return new StickySessionLoadBalancingStrategy(activeOrigins, loadBalancer); - } else if (originsRestrictionCookie == null){ - return loadBalancer; - } else { - return new OriginRestrictionLoadBalancingStrategy(activeOrigins, loadBalancer); - } - } - - private static void routeDatabaseChanged(String appId, ObjectStore snapshot, AtomicReference> remoteHosts) { - Set newSet = snapshot.entrySet() - .stream() - .filter(it -> isTaggedWith(it, appId)) - .map(it -> toRemoteHost(appId, it)) - .collect(Collectors.toSet()); - - remoteHosts.set(newSet); - } - - private static boolean isTaggedWith(Map.Entry recordEntry, String appId) { - return recordEntry.getValue().getTags().contains(appId); - } - - private static RemoteHost toRemoteHost(String appId, Map.Entry record) { - RoutingObjectAdapter routingObject = record.getValue().getRoutingObject(); - String originName = record.getKey(); - - return remoteHost( - // The origin is used to determine remote host hostname or port - // therefore we'll just pass NA:0 - newOriginBuilder("na", 0) - .applicationId(appId) - .id(originName) - .build(), - routingObject, - routingObject::metric); - } - - private static void watchFailed(String name, Throwable cause) { - LOGGER.error("{} watch error - cause={}", name, cause); - } - - private static void watchCompleted(String name) { - LOGGER.error("{} watch complete", name); - } - - } -} diff --git a/components/proxy/src/main/kotlin/com/hotels/styx/routing/handlers/LoadBalancingGroup.kt b/components/proxy/src/main/kotlin/com/hotels/styx/routing/handlers/LoadBalancingGroup.kt new file mode 100644 index 0000000000..481fec4ef2 --- /dev/null +++ b/components/proxy/src/main/kotlin/com/hotels/styx/routing/handlers/LoadBalancingGroup.kt @@ -0,0 +1,164 @@ +/* + Copyright (C) 2013-2019 Expedia 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 com.hotels.styx.routing.handlers + +import com.fasterxml.jackson.annotation.JsonProperty +import com.hotels.styx.api.Eventual +import com.hotels.styx.api.HttpInterceptor +import com.hotels.styx.api.Id +import com.hotels.styx.api.LiveHttpRequest +import com.hotels.styx.api.configuration.ObjectStore +import com.hotels.styx.api.extension.ActiveOrigins +import com.hotels.styx.api.extension.Origin.newOriginBuilder +import com.hotels.styx.api.extension.RemoteHost +import com.hotels.styx.api.extension.RemoteHost.remoteHost +import com.hotels.styx.api.extension.loadbalancing.spi.LoadBalancer +import com.hotels.styx.api.extension.loadbalancing.spi.LoadBalancingMetricSupplier +import com.hotels.styx.api.extension.service.StickySessionConfig +import com.hotels.styx.client.OriginRestrictionLoadBalancingStrategy +import com.hotels.styx.client.StyxBackendServiceClient +import com.hotels.styx.client.loadbalancing.strategies.PowerOfTwoStrategy +import com.hotels.styx.client.stickysession.StickySessionLoadBalancingStrategy +import com.hotels.styx.config.schema.SchemaDsl.`object` +import com.hotels.styx.config.schema.SchemaDsl.bool +import com.hotels.styx.config.schema.SchemaDsl.field +import com.hotels.styx.config.schema.SchemaDsl.integer +import com.hotels.styx.config.schema.SchemaDsl.optional +import com.hotels.styx.config.schema.SchemaDsl.string +import com.hotels.styx.infrastructure.configuration.yaml.JsonNodeConfig +import com.hotels.styx.routing.RoutingObject +import com.hotels.styx.routing.RoutingObjectRecord +import com.hotels.styx.routing.config.HttpHandlerFactory +import com.hotels.styx.routing.config.RoutingObjectDefinition +import org.slf4j.LoggerFactory +import reactor.core.Disposable +import reactor.core.publisher.toFlux +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletableFuture.completedFuture +import java.util.concurrent.atomic.AtomicReference + +class LoadBalancingGroup(val client: StyxBackendServiceClient, val changeWatcher: Disposable) : RoutingObject { + + override fun handle(request: LiveHttpRequest?, context: HttpInterceptor.Context?) = Eventual(client.sendRequest(request)) + + override fun stop(): CompletableFuture { + changeWatcher.dispose() + return completedFuture(null) + } + + companion object { + val SCHEMA = `object`( + field("origins", string()), + optional("originsRestrictionCookie", string()), + optional("stickySession", `object`( + field("enabled", bool()), + field("timeoutSeconds", integer()) + )) + ) + + private val LOGGER = LoggerFactory.getLogger(LoadBalancingGroup::class.java) + + } + + class Factory : HttpHandlerFactory { + override fun build(parents: List, context: HttpHandlerFactory.Context, configBlock: RoutingObjectDefinition): RoutingObject { + + val appId = parents.last() + val config = JsonNodeConfig(configBlock.config()).`as`(Config::class.java) + + val routeDb = context.routeDb() + val remoteHosts = AtomicReference>(setOf()) + + val watch = routeDb.watch() + .toFlux() + .subscribe( + { routeDatabaseChanged(config.origins, it, remoteHosts) }, + { watchFailed(appId, it) }, + { watchCompleted(appId) } + ) + + + val client = StyxBackendServiceClient.Builder(Id.id(appId)) + .loadBalancer(loadBalancer(config, ActiveOrigins { remoteHosts.get() })) + .metricsRegistry(context.environment().metricRegistry()) + .originIdHeader(context.environment().configuration().styxHeaderConfig().originIdHeaderName()) + .stickySessionConfig(config.stickySession ?: StickySessionConfig.stickySessionDisabled()) + .originsRestrictionCookieName(config.originsRestrictionCookie) + .build() + + return LoadBalancingGroup(client, watch) + } + + private fun loadBalancer(config: Config, activeOrigins: ActiveOrigins): LoadBalancer { + val loadBalancer = PowerOfTwoStrategy(activeOrigins) + return if (config.stickySessionConfig.stickySessionEnabled()) { + StickySessionLoadBalancingStrategy(activeOrigins, loadBalancer) + } else if (config.originsRestrictionCookie == null) { + loadBalancer + } else { + OriginRestrictionLoadBalancingStrategy(activeOrigins, loadBalancer) + } + } + + private fun routeDatabaseChanged(appId: String, snapshot: ObjectStore, remoteHosts: AtomicReference>) { + val newSet = snapshot.entrySet() + .filter { isTaggedWith(it, appId) } + .map { toRemoteHost(appId, it) } + .toSet() + + remoteHosts.set(newSet) + } + + private fun isTaggedWith(recordEntry: Map.Entry, appId: String): Boolean { + return recordEntry.value.tags.contains(appId) + } + + private fun toRemoteHost(appId: String, record: Map.Entry): RemoteHost { + val routingObject = record.value.routingObject + val originName = record.key + + return remoteHost( + // The origin is used to determine remote host hostname or port + // therefore we'll just pass NA:0 + newOriginBuilder("NA", 0) + .applicationId(appId) + .id(originName) + .build(), + routingObject, + LoadBalancingMetricSupplier { routingObject.metric() }) + } + + private fun watchFailed(name: String, cause: Throwable) { + LOGGER.error("{} watch error - cause={}", name, cause) + } + + private fun watchCompleted(name: String) { + LOGGER.error("{} watch complete", name) + } + + } + + data class Config( + @JsonProperty val origins: String, + @JsonProperty val strategy: String?, + @JsonProperty val originsRestrictionCookie: String?, + @JsonProperty val stickySession: StickySessionConfig? + ) { + val stickySessionConfig: StickySessionConfig + get() = stickySession ?: StickySessionConfig.stickySessionDisabled() + } + +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 7d623ca340..9e35ff8701 100755 --- a/pom.xml +++ b/pom.xml @@ -97,6 +97,7 @@ 2.9.9 2.9.9 2.9.9 + 2.9.9 4.0.5 4.5.1-1 4.1.15.Final @@ -313,6 +314,12 @@ ${jackson.module.scala.version} + + com.fasterxml.jackson.module + jackson-module-kotlin + ${jackson-module-kotlin.version} + + io.dropwizard.metrics diff --git a/system-tests/ft-suite/pom.xml b/system-tests/ft-suite/pom.xml index 3998fc7eaf..1124af0bc2 100644 --- a/system-tests/ft-suite/pom.xml +++ b/system-tests/ft-suite/pom.xml @@ -23,7 +23,6 @@ 2.9.8 2.5.1 - 2.9.7 1.3.2 diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/routing/LoadBalancingGroupSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/routing/LoadBalancingGroupSpec.kt index a0ec5e37bd..97561ef2c9 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/routing/LoadBalancingGroupSpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/routing/LoadBalancingGroupSpec.kt @@ -338,9 +338,9 @@ class LoadBalancingGroupSpec : FeatureSpec() { } } - scenario("!Routes to new origin when the origin indicated by sticky session cookie is no longer available") { - TODO("Styx doesn't support this as of today") - } +// scenario("!Routes to new origin when the origin indicated by sticky session cookie is no longer available") { +// TODO("Styx doesn't support this as of today") +// } } feature("Origins restriction") {