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") {