From faceb5f419f787ff37c0954fb46943a92e5d46d2 Mon Sep 17 00:00:00 2001 From: Eric Zhao Date: Mon, 22 Apr 2019 16:16:03 +0800 Subject: [PATCH] Add sentinel-api-gateway-adapter-common module for universal gateway rule and API definition management Signed-off-by: Eric Zhao --- sentinel-adapter/pom.xml | 6 + .../README.md | 8 + .../pom.xml | 36 ++++ .../common/SentinelGatewayConstants.java | 45 +++++ .../gateway/common/api/ApiDefinition.java | 79 ++++++++ .../api/ApiDefinitionChangeObserver.java | 32 +++ .../common/api/ApiPathPredicateItem.java | 72 +++++++ .../common/api/ApiPredicateGroupItem.java | 46 +++++ .../gateway/common/api/ApiPredicateItem.java | 37 ++++ .../api/GatewayApiDefinitionManager.java | 162 +++++++++++++++ .../api/matcher/AbstractApiMatcher.java | 74 +++++++ .../common/param/GatewayParamParser.java | 145 ++++++++++++++ .../common/param/RequestItemParser.java | 57 ++++++ .../gateway/common/rule/GatewayFlowRule.java | 186 +++++++++++++++++ .../common/rule/GatewayParamFlowItem.java | 103 ++++++++++ .../common/rule/GatewayRuleConverter.java | 60 ++++++ .../common/rule/GatewayRuleManager.java | 187 ++++++++++++++++++ .../api/GatewayApiDefinitionManagerTest.java | 27 +++ .../common/rule/GatewayRuleConverterTest.java | 64 ++++++ .../common/rule/GatewayRuleManagerTest.java | 115 +++++++++++ 20 files changed, 1541 insertions(+) create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/README.md create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/pom.xml create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/SentinelGatewayConstants.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiDefinition.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiDefinitionChangeObserver.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPathPredicateItem.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPredicateGroupItem.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPredicateItem.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/GatewayApiDefinitionManager.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/matcher/AbstractApiMatcher.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayParamParser.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/RequestItemParser.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayFlowRule.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayParamFlowItem.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleConverter.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleManager.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/GatewayApiDefinitionManagerTest.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleConverterTest.java create mode 100644 sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleManagerTest.java diff --git a/sentinel-adapter/pom.xml b/sentinel-adapter/pom.xml index c573b8b2..13e516cb 100755 --- a/sentinel-adapter/pom.xml +++ b/sentinel-adapter/pom.xml @@ -22,6 +22,7 @@ sentinel-zuul-adapter sentinel-reactor-adapter sentinel-spring-webflux-adapter + sentinel-api-gateway-adapter-common @@ -46,6 +47,11 @@ sentinel-reactor-adapter ${project.version} + + com.alibaba.csp + sentinel-api-gateway-adapter-common + ${project.version} + junit diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/README.md b/sentinel-adapter/sentinel-api-gateway-adapter-common/README.md new file mode 100644 index 00000000..9749b443 --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/README.md @@ -0,0 +1,8 @@ +# Sentinel API Gateway Adapter Common + +The `sentinel-api-gateway-adapter-common` module provides common abstraction for +API gateway flow control: + +- `GatewayFlowRule`: flow control rule specific for route or API defined in API gateway. +This can be automatically converted to `FlowRule` or `ParamFlowRule`. +- `ApiDefinition`: gateway API definition with a group of predicates \ No newline at end of file diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/pom.xml b/sentinel-adapter/sentinel-api-gateway-adapter-common/pom.xml new file mode 100644 index 00000000..acda0b1d --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/pom.xml @@ -0,0 +1,36 @@ + + + + sentinel-adapter + com.alibaba.csp + 1.6.0-SNAPSHOT + + 4.0.0 + + sentinel-api-gateway-adapter-common + jar + + + 1.7 + 1.7 + + + + + com.alibaba.csp + sentinel-core + + + com.alibaba.csp + sentinel-parameter-flow-control + + + + junit + junit + test + + + \ No newline at end of file diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/SentinelGatewayConstants.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/SentinelGatewayConstants.java new file mode 100644 index 00000000..4a2c6ce3 --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/SentinelGatewayConstants.java @@ -0,0 +1,45 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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.alibaba.csp.sentinel.adapter.gateway.common; + +/** + * @author Eric Zhao + * @since 1.6.0 + */ +public final class SentinelGatewayConstants { + + public static final int APP_TYPE_GATEWAY = 1; + + public static final int RESOURCE_MODE_ROUTE_ID = 0; + public static final int RESOURCE_MODE_CUSTOM_API_NAME = 1; + + public static final int PARAM_PARSE_STRATEGY_CLIENT_IP = 0; + public static final int PARAM_PARSE_STRATEGY_HOST = 1; + public static final int PARAM_PARSE_STRATEGY_HEADER = 2; + public static final int PARAM_PARSE_STRATEGY_URL_PARAM = 3; + + public static final int PARAM_MATCH_STRATEGY_EXACT = 0; + public static final int PARAM_MATCH_STRATEGY_PREFIX = 1; + public static final int PARAM_MATCH_STRATEGY_REGEX = 2; + + public static final String GATEWAY_CONTEXT_DEFAULT = "sentinel_gateway_context_default"; + public static final String GATEWAY_CONTEXT_PREFIX = "sentinel_gateway_context$$"; + public static final String GATEWAY_CONTEXT_ROUTE_PREFIX = "sentinel_gateway_context$$route$$"; + + public static final String GATEWAY_NOT_MATCH_PARAM = "$$not_match"; + + private SentinelGatewayConstants() {} +} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiDefinition.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiDefinition.java new file mode 100644 index 00000000..6f3a9782 --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiDefinition.java @@ -0,0 +1,79 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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.alibaba.csp.sentinel.adapter.gateway.common.api; + +import java.util.Objects; +import java.util.Set; + +/** + * @author Eric Zhao + * @since 1.6.0 + */ +public class ApiDefinition { + + private String apiName; + private Set predicateItems; + + public ApiDefinition() {} + + public ApiDefinition(String apiName) { + this.apiName = apiName; + } + + public String getApiName() { + return apiName; + } + + public ApiDefinition setApiName(String apiName) { + this.apiName = apiName; + return this; + } + + public Set getPredicateItems() { + return predicateItems; + } + + public ApiDefinition setPredicateItems(Set predicateItems) { + this.predicateItems = predicateItems; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + + ApiDefinition that = (ApiDefinition)o; + + if (!Objects.equals(apiName, that.apiName)) { return false; } + return Objects.equals(predicateItems, that.predicateItems); + } + + @Override + public int hashCode() { + int result = apiName != null ? apiName.hashCode() : 0; + result = 31 * result + (predicateItems != null ? predicateItems.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "ApiDefinition{" + + "apiName='" + apiName + '\'' + + ", predicateItems=" + predicateItems + + '}'; + } +} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiDefinitionChangeObserver.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiDefinitionChangeObserver.java new file mode 100644 index 00000000..5cc9cdc3 --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiDefinitionChangeObserver.java @@ -0,0 +1,32 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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 + * + * https://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.alibaba.csp.sentinel.adapter.gateway.common.api; + +import java.util.Set; + +/** + * @author Eric Zhao + * @since 1.6.0 + */ +public interface ApiDefinitionChangeObserver { + + /** + * Notify the observer about the new gateway API definitions. + * + * @param apiDefinitions new set of gateway API definition + */ + void onChange(Set apiDefinitions); +} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPathPredicateItem.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPathPredicateItem.java new file mode 100644 index 00000000..1cbce589 --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPathPredicateItem.java @@ -0,0 +1,72 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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.alibaba.csp.sentinel.adapter.gateway.common.api; + +import java.util.Objects; + +/** + * @author Eric Zhao + * @since 1.6.0 + */ +public class ApiPathPredicateItem implements ApiPredicateItem { + + private String pattern; + private int matchStrategy; + + public ApiPathPredicateItem setPattern(String pattern) { + this.pattern = pattern; + return this; + } + + public ApiPathPredicateItem setMatchStrategy(int matchStrategy) { + this.matchStrategy = matchStrategy; + return this; + } + + public String getPattern() { + return pattern; + } + + public int getMatchStrategy() { + return matchStrategy; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + + ApiPathPredicateItem that = (ApiPathPredicateItem)o; + + if (matchStrategy != that.matchStrategy) { return false; } + return Objects.equals(pattern, that.pattern); + } + + @Override + public int hashCode() { + int result = pattern != null ? pattern.hashCode() : 0; + result = 31 * result + matchStrategy; + return result; + } + + @Override + public String toString() { + return "ApiPathPredicateItem{" + + "pattern='" + pattern + '\'' + + ", matchStrategy=" + matchStrategy + + '}'; + } +} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPredicateGroupItem.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPredicateGroupItem.java new file mode 100644 index 00000000..c45322c2 --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPredicateGroupItem.java @@ -0,0 +1,46 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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 + * + * https://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.alibaba.csp.sentinel.adapter.gateway.common.api; + +import java.util.HashSet; +import java.util.Set; + +import com.alibaba.csp.sentinel.util.AssertUtil; + +/** + * @author Eric Zhao + * @since 1.6.0 + */ +public class ApiPredicateGroupItem implements ApiPredicateItem { + + private final Set items = new HashSet<>(); + + public ApiPredicateGroupItem addItem(ApiPredicateItem item) { + AssertUtil.notNull(item, "item cannot be null"); + items.add(item); + return this; + } + + public Set getItems() { + return items; + } + + /*@Override + public ApiPredicateItem and(ApiPredicateItem item) { + AssertUtil.notNull(item, "item cannot be null"); + return this.addItem(item); + }*/ +} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPredicateItem.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPredicateItem.java new file mode 100644 index 00000000..d42bc5d6 --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/ApiPredicateItem.java @@ -0,0 +1,37 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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.alibaba.csp.sentinel.adapter.gateway.common.api; + +import com.alibaba.csp.sentinel.util.AssertUtil; + +/** + * @author Eric Zhao + * @since 1.6.0 + */ +public interface ApiPredicateItem { + + /** + * Combine two {@link ApiPredicateItem}. + * + * @param item another predicate item + * @return combined predicate group item + */ + /*default ApiPredicateItem and(ApiPredicateItem item) { + AssertUtil.notNull(item, "item cannot be null"); + return new ApiPredicateGroupItem() + .addItem(this).addItem(item); + }*/ +} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/GatewayApiDefinitionManager.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/GatewayApiDefinitionManager.java new file mode 100644 index 00000000..3985e7d9 --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/GatewayApiDefinitionManager.java @@ -0,0 +1,162 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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 + * + * https://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.alibaba.csp.sentinel.adapter.gateway.common.api; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.property.DynamicSentinelProperty; +import com.alibaba.csp.sentinel.property.PropertyListener; +import com.alibaba.csp.sentinel.property.SentinelProperty; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.csp.sentinel.util.SpiLoader; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * Manager for gateway API definitions. + * + * @author Eric Zhao + * @since 1.6.0 + */ +public final class GatewayApiDefinitionManager { + + private static final Map API_MAP = new ConcurrentHashMap<>(); + + private static final ApiDefinitionPropertyListener LISTENER = new ApiDefinitionPropertyListener(); + private static SentinelProperty> currentProperty = new DynamicSentinelProperty<>(); + + /** + * The map keeps all found ApiDefinitionChangeObserver (class name as key). + */ + private static final Map API_CHANGE_OBSERVERS = new ConcurrentHashMap<>(); + + static { + try { + currentProperty.addListener(LISTENER); + initializeApiChangeObserverSpi(); + } catch (Throwable ex) { + RecordLog.warn("[GatewayApiDefinitionManager] Failed to initialize", ex); + ex.printStackTrace(); + } + } + + private static void initializeApiChangeObserverSpi() { + List listeners = SpiLoader.loadInstanceList(ApiDefinitionChangeObserver.class); + for (ApiDefinitionChangeObserver e : listeners) { + API_CHANGE_OBSERVERS.put(e.getClass().getCanonicalName(), e); + RecordLog.info("[GatewayApiDefinitionManager] ApiDefinitionChangeObserver added: {0}", + e.getClass().getCanonicalName()); + } + } + + public static void register2Property(SentinelProperty> property) { + AssertUtil.notNull(property, "property cannot be null"); + synchronized (LISTENER) { + RecordLog.info("[GatewayApiDefinitionManager] Registering new property to gateway API definition manager"); + currentProperty.removeListener(LISTENER); + property.addListener(LISTENER); + currentProperty = property; + } + } + + /** + * Load given gateway API definitions and apply to downstream observers. + * + * @param apiDefinitions set of gateway API definitions + * @return true if updated, or else false + */ + public static boolean loadApiDefinitions(Set apiDefinitions) { + return currentProperty.updateValue(apiDefinitions); + } + + public static ApiDefinition getApiDefinition(final String apiName) { + if (apiName == null) { + return null; + } + return API_MAP.get(apiName); + } + + public static Set getApiDefinitions() { + return new HashSet<>(API_MAP.values()); + } + + private static final class ApiDefinitionPropertyListener implements PropertyListener> { + + @Override + public void configUpdate(Set set) { + applyApiUpdateInternal(set); + RecordLog.info("[GatewayApiDefinitionManager] Api definition updated: " + API_MAP); + } + + @Override + public void configLoad(Set set) { + applyApiUpdateInternal(set); + RecordLog.info("[GatewayApiDefinitionManager] Api definition loaded: " + API_MAP); + } + + private static synchronized void applyApiUpdateInternal(Set set) { + if (set == null || set.isEmpty()) { + API_MAP.clear(); + notifyDownstreamListeners(new HashSet()); + return; + } + Map map = new HashMap<>(set.size()); + Set validSet = new HashSet<>(); + for (ApiDefinition definition : set) { + if (isValidApi(definition)) { + map.put(definition.getApiName(), definition); + validSet.add(definition); + } + } + + API_MAP.clear(); + API_MAP.putAll(map); + + // propagate to downstream. + notifyDownstreamListeners(validSet); + } + } + + private static void notifyDownstreamListeners(/*@Valid*/ final Set definitions) { + try { + for (Map.Entry entry : API_CHANGE_OBSERVERS.entrySet()) { + entry.getValue().onChange(definitions); + } + } catch (Exception ex) { + RecordLog.warn("[GatewayApiDefinitionManager] WARN: failed to notify downstream api listeners", ex); + } + } + + public static boolean isValidApi(ApiDefinition apiDefinition) { + return apiDefinition != null && StringUtil.isNotBlank(apiDefinition.getApiName()) + && apiDefinition.getPredicateItems() != null; + } + + static void addApiChangeListener(ApiDefinitionChangeObserver listener) { + AssertUtil.notNull(listener, "listener cannot be null"); + API_CHANGE_OBSERVERS.put(listener.getClass().getCanonicalName(), listener); + } + + static void removeApiChangeListener(Class clazz) { + AssertUtil.notNull(clazz, "class cannot be null"); + API_CHANGE_OBSERVERS.remove(clazz.getCanonicalName()); + } +} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/matcher/AbstractApiMatcher.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/matcher/AbstractApiMatcher.java new file mode 100644 index 00000000..7fa081ed --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/matcher/AbstractApiMatcher.java @@ -0,0 +1,74 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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 + * + * https://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.alibaba.csp.sentinel.adapter.gateway.common.api.matcher; + +import java.util.HashSet; +import java.util.Set; + +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.csp.sentinel.util.function.Predicate; + +/** + * @author Eric Zhao + * @since 1.6.0 + */ +public abstract class AbstractApiMatcher implements Predicate { + + protected final String apiName; + protected final ApiDefinition apiDefinition; + /** + * We use {@link com.alibaba.csp.sentinel.util.function.Predicate} here as the min JDK version is 1.7. + */ + protected final Set> matchers = new HashSet<>(); + + public AbstractApiMatcher(ApiDefinition apiDefinition) { + AssertUtil.notNull(apiDefinition, "apiDefinition cannot be null"); + AssertUtil.assertNotBlank(apiDefinition.getApiName(), "apiName cannot be empty"); + this.apiName = apiDefinition.getApiName(); + this.apiDefinition = apiDefinition; + + try { + initializeMatchers(); + } catch (Exception ex) { + RecordLog.warn("[GatewayApiMatcher] Failed to initialize internal matchers", ex); + } + } + + /** + * Initialize the matchers. + */ + protected abstract void initializeMatchers(); + + @Override + public boolean test(T t) { + for (Predicate matcher : matchers) { + if (matcher.test(t)) { + return true; + } + } + return false; + } + + public String getApiName() { + return apiName; + } + + public ApiDefinition getApiDefinition() { + return apiDefinition; + } +} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayParamParser.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayParamParser.java new file mode 100644 index 00000000..ee3dfaaf --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/GatewayParamParser.java @@ -0,0 +1,145 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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 + * + * https://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.alibaba.csp.sentinel.adapter.gateway.common.param; + +import java.util.HashSet; +import java.util.Set; + +import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayParamFlowItem; +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.csp.sentinel.util.function.Predicate; + +/** + * @author Eric Zhao + * @since 1.6.0 + */ +public class GatewayParamParser { + + private final RequestItemParser requestItemParser; + + public GatewayParamParser(RequestItemParser requestItemParser) { + AssertUtil.notNull(requestItemParser, "requestItemParser cannot be null"); + this.requestItemParser = requestItemParser; + } + + /** + * Parse parameters for given resource from the request entity on condition of the rule predicate. + * + * @param resource valid resource name + * @param request valid request + * @param rulePredicate rule predicate indicating the rules to refer + * @return the parameter array + */ + public Object[] parseParameterFor(String resource, T request, Predicate rulePredicate) { + if (StringUtil.isEmpty(resource) || request == null || rulePredicate == null) { + return new Object[0]; + } + Set gatewayRules = new HashSet<>(); + Set predSet = new HashSet<>(); + for (GatewayFlowRule rule : GatewayRuleManager.getRulesForResource(resource)) { + if (rule.getParamItem() != null) { + gatewayRules.add(rule); + predSet.add(rulePredicate.test(rule)); + } + } + if (gatewayRules.isEmpty()) { + return new Object[0]; + } + if (predSet.size() != 1 || predSet.contains(false)) { + return new Object[0]; + } + Object[] arr = new Object[gatewayRules.size()]; + for (GatewayFlowRule rule : gatewayRules) { + GatewayParamFlowItem paramItem = rule.getParamItem(); + int idx = paramItem.getIndex(); + String param = parseInternal(paramItem, request); + arr[idx] = param; + } + return arr; + } + + private String parseInternal(GatewayParamFlowItem item, T request) { + switch (item.getParseStrategy()) { + case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_CLIENT_IP: + return parseClientIp(item, request); + case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HOST: + return parseHost(item, request); + case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HEADER: + return parseHeader(item, request); + case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM: + return parseUrlParameter(item, request); + default: + return null; + } + } + + private String parseClientIp(/*@Valid*/ GatewayParamFlowItem item, T request) { + String clientIp = requestItemParser.getRemoteAddress(request); + String pattern = item.getPattern(); + if (pattern == null) { + return clientIp; + } + return parseWithMatchStrategyInternal(item.getMatchStrategy(), clientIp, pattern); + } + + private String parseHeader(/*@Valid*/ GatewayParamFlowItem item, T request) { + String headerKey = item.getFieldName(); + String pattern = item.getPattern(); + // TODO: what if the header has multiple values? + String headerValue = requestItemParser.getHeader(request, headerKey); + if (pattern == null) { + return headerValue; + } + // Match value according to regex pattern or exact mode. + return parseWithMatchStrategyInternal(item.getMatchStrategy(), headerValue, pattern); + } + + private String parseHost(/*@Valid*/ GatewayParamFlowItem item, T request) { + String pattern = item.getPattern(); + String host = requestItemParser.getHeader(request, "Host"); + if (pattern == null) { + return host; + } + // Match value according to regex pattern or exact mode. + return parseWithMatchStrategyInternal(item.getMatchStrategy(), host, pattern); + } + + private String parseUrlParameter(/*@Valid*/ GatewayParamFlowItem item, T request) { + String paramName = item.getFieldName(); + String pattern = item.getPattern(); + String param = requestItemParser.getUrlParam(request, paramName); + if (pattern == null) { + return param; + } + // Match value according to regex pattern or exact mode. + return parseWithMatchStrategyInternal(item.getMatchStrategy(), param, pattern); + } + + private String parseWithMatchStrategyInternal(int matchStrategy, String value, String pattern) { + // TODO: implement here. + if (value == null) { + return null; + } + if (matchStrategy == SentinelGatewayConstants.PARAM_MATCH_STRATEGY_REGEX) { + return value; + } + return value; + } +} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/RequestItemParser.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/RequestItemParser.java new file mode 100644 index 00000000..5b28abe7 --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/param/RequestItemParser.java @@ -0,0 +1,57 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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 + * + * https://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.alibaba.csp.sentinel.adapter.gateway.common.param; + +/** + * @author Eric Zhao + * @since 1.6.0 + */ +public interface RequestItemParser { + + /** + * Get API path from the request. + * + * @param request valid request + * @return API path + */ + String getPath(T request); + + /** + * Get remote address from the request. + * + * @param request valid request + * @return remote address + */ + String getRemoteAddress(T request); + + /** + * Get the header associated with the header key. + * + * @param request valid request + * @param key valid header key + * @return the header + */ + String getHeader(T request, String key); + + /** + * Get the parameter value associated with the parameter name. + * + * @param request valid request + * @param paramName valid parameter name + * @return the parameter value + */ + String getUrlParam(T request, String paramName); +} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayFlowRule.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayFlowRule.java new file mode 100644 index 00000000..0486d48a --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayFlowRule.java @@ -0,0 +1,186 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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 + * + * https://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.alibaba.csp.sentinel.adapter.gateway.common.rule; + +import java.util.Objects; + +import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; + +/** + * @author Eric Zhao + * @since 1.6.0 + */ +public class GatewayFlowRule { + + private String resource; + private int resourceMode = SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID; + + private int grade = RuleConstant.FLOW_GRADE_QPS; + private double count; + private long intervalSec = 1; + + private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT; + private int burst; + /** + * For throttle (rate limiting with queueing). + */ + private int maxQueueingTimeoutMs = 500; + + /** + * For parameter flow control. If not set, the gateway rule will be + * converted to normal flow rule. + */ + private GatewayParamFlowItem paramItem; + + public GatewayFlowRule() {} + + public GatewayFlowRule(String resource) { + this.resource = resource; + } + + public String getResource() { + return resource; + } + + public GatewayFlowRule setResource(String resource) { + this.resource = resource; + return this; + } + + public int getResourceMode() { + return resourceMode; + } + + public GatewayFlowRule setResourceMode(int resourceMode) { + this.resourceMode = resourceMode; + return this; + } + + public int getGrade() { + return grade; + } + + public GatewayFlowRule setGrade(int grade) { + this.grade = grade; + return this; + } + + public int getControlBehavior() { + return controlBehavior; + } + + public GatewayFlowRule setControlBehavior(int controlBehavior) { + this.controlBehavior = controlBehavior; + return this; + } + + public double getCount() { + return count; + } + + public GatewayFlowRule setCount(double count) { + this.count = count; + return this; + } + + public long getIntervalSec() { + return intervalSec; + } + + public GatewayFlowRule setIntervalSec(long intervalSec) { + this.intervalSec = intervalSec; + return this; + } + + public int getBurst() { + return burst; + } + + public GatewayFlowRule setBurst(int burst) { + this.burst = burst; + return this; + } + + public GatewayParamFlowItem getParamItem() { + return paramItem; + } + + public GatewayFlowRule setParamItem(GatewayParamFlowItem paramItem) { + this.paramItem = paramItem; + return this; + } + + public int getMaxQueueingTimeoutMs() { + return maxQueueingTimeoutMs; + } + + public GatewayFlowRule setMaxQueueingTimeoutMs(int maxQueueingTimeoutMs) { + this.maxQueueingTimeoutMs = maxQueueingTimeoutMs; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + + GatewayFlowRule rule = (GatewayFlowRule)o; + + if (resourceMode != rule.resourceMode) { return false; } + if (grade != rule.grade) { return false; } + if (Double.compare(rule.count, count) != 0) { return false; } + if (intervalSec != rule.intervalSec) { return false; } + if (controlBehavior != rule.controlBehavior) { return false; } + if (burst != rule.burst) { return false; } + if (maxQueueingTimeoutMs != rule.maxQueueingTimeoutMs) { return false; } + if (!Objects.equals(resource, rule.resource)) { return false; } + return Objects.equals(paramItem, rule.paramItem); + + } + + @Override + public int hashCode() { + int result; + long temp; + result = resource != null ? resource.hashCode() : 0; + result = 31 * result + resourceMode; + result = 31 * result + grade; + temp = Double.doubleToLongBits(count); + result = 31 * result + (int)(temp ^ (temp >>> 32)); + result = 31 * result + (int)(intervalSec ^ (intervalSec >>> 32)); + result = 31 * result + controlBehavior; + result = 31 * result + burst; + result = 31 * result + maxQueueingTimeoutMs; + result = 31 * result + (paramItem != null ? paramItem.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "GatewayFlowRule{" + + "resource='" + resource + '\'' + + ", resourceMode=" + resourceMode + + ", grade=" + grade + + ", count=" + count + + ", intervalSec=" + intervalSec + + ", controlBehavior=" + controlBehavior + + ", burst=" + burst + + ", maxQueueingTimeoutMs=" + maxQueueingTimeoutMs + + ", paramItem=" + paramItem + + '}'; + } +} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayParamFlowItem.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayParamFlowItem.java new file mode 100644 index 00000000..11e4a908 --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayParamFlowItem.java @@ -0,0 +1,103 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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 + * + * https://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.alibaba.csp.sentinel.adapter.gateway.common.rule; + +import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; + +/** + * @author Eric Zhao + * @since 1.6.0 + */ +public class GatewayParamFlowItem { + + /** + * Should be set when applying to parameter flow rules. + */ + private Integer index; + + /** + * Strategy for parsing item (e.g. client IP, arbitrary headers and URL parameters). + */ + private int parseStrategy; + /** + * Field to get (only required for arbitrary headers or URL parameters mode). + */ + private String fieldName; + /** + * Matching pattern. If not set, all values will be kept in LRU map. + */ + private String pattern; + /** + * Matching strategy for item value. + */ + private int matchStrategy = SentinelGatewayConstants.PARAM_MATCH_STRATEGY_EXACT; + + public Integer getIndex() { + return index; + } + + GatewayParamFlowItem setIndex(Integer index) { + this.index = index; + return this; + } + + public int getParseStrategy() { + return parseStrategy; + } + + public GatewayParamFlowItem setParseStrategy(int parseStrategy) { + this.parseStrategy = parseStrategy; + return this; + } + + public String getFieldName() { + return fieldName; + } + + public GatewayParamFlowItem setFieldName(String fieldName) { + this.fieldName = fieldName; + return this; + } + + public String getPattern() { + return pattern; + } + + public GatewayParamFlowItem setPattern(String pattern) { + this.pattern = pattern; + return this; + } + + public int getMatchStrategy() { + return matchStrategy; + } + + public GatewayParamFlowItem setMatchStrategy(int matchStrategy) { + this.matchStrategy = matchStrategy; + return this; + } + + @Override + public String toString() { + return "GatewayParamFlowItem{" + + "index=" + index + + ", parseStrategy=" + parseStrategy + + ", fieldName='" + fieldName + '\'' + + ", pattern='" + pattern + '\'' + + ", matchStrategy=" + matchStrategy + + '}'; + } +} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleConverter.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleConverter.java new file mode 100644 index 00000000..e2b091ba --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleConverter.java @@ -0,0 +1,60 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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 + * + * https://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.alibaba.csp.sentinel.adapter.gateway.common.rule; + +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; + +/** + * @author Eric Zhao + * @since 1.6.0 + */ +final class GatewayRuleConverter { + + static FlowRule toFlowRule(/*@Valid*/ GatewayFlowRule rule) { + return new FlowRule(rule.getResource()) + .setControlBehavior(rule.getControlBehavior()) + .setCount(rule.getCount()) + .setGrade(rule.getGrade()) + .setMaxQueueingTimeMs(rule.getMaxQueueingTimeoutMs()); + } + + /** + * Convert a gateway rule to parameter flow rule, then apply the generated + * parameter index to {@link GatewayParamFlowItem} of the rule. + * + * @param gatewayRule a valid gateway rule that should contain valid parameter items + * @param idx generated parameter index (callers should guarantee it's unique and incremental) + * @return converted parameter flow rule + */ + static ParamFlowRule applyToParamRule(/*@Valid*/ GatewayFlowRule gatewayRule, int idx) { + ParamFlowRule paramRule = new ParamFlowRule(gatewayRule.getResource()) + .setCount(gatewayRule.getCount()) + .setGrade(gatewayRule.getGrade()) + .setDurationInSec(gatewayRule.getIntervalSec()) + .setBurstCount(gatewayRule.getBurst()) + .setControlBehavior(gatewayRule.getControlBehavior()) + .setMaxQueueingTimeMs(gatewayRule.getMaxQueueingTimeoutMs()) + .setParamIdx(idx); + GatewayParamFlowItem gatewayItem = gatewayRule.getParamItem(); + // Apply the current idx to gateway rule item. + gatewayItem.setIndex(idx); + // TODO: implement for pattern-based parameters. + return paramRule; + } + + private GatewayRuleConverter() {} +} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleManager.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleManager.java new file mode 100644 index 00000000..f0ca5fd7 --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleManager.java @@ -0,0 +1,187 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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.alibaba.csp.sentinel.adapter.gateway.common.rule; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.property.DynamicSentinelProperty; +import com.alibaba.csp.sentinel.property.PropertyListener; +import com.alibaba.csp.sentinel.property.SentinelProperty; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * @author Eric Zhao + * @since 1.6.0 + */ +public final class GatewayRuleManager { + + private static final Map> RULE_MAP = new ConcurrentHashMap<>(); + + private static final GatewayRulePropertyListener LISTENER = new GatewayRulePropertyListener(); + private static SentinelProperty> currentProperty = new DynamicSentinelProperty<>(); + + static { + currentProperty.addListener(LISTENER); + } + + public static void register2Property(SentinelProperty> property) { + AssertUtil.notNull(property, "property cannot be null"); + synchronized (LISTENER) { + RecordLog.info("[GatewayRuleManager] Registering new property to gateway flow rule manager"); + currentProperty.removeListener(LISTENER); + property.addListener(LISTENER); + currentProperty = property; + } + } + + /** + * Load all provided gateway rules into memory, while + * previous rules will be replaced. + * + * @param rules rule set + * @return true if updated, otherwise false + */ + public static boolean loadRules(Set rules) { + return currentProperty.updateValue(rules); + } + + public static Set getRules() { + Set rules = new HashSet<>(); + for (Set ruleSet : RULE_MAP.values()) { + rules.addAll(ruleSet); + } + return rules; + } + + public static Set getRulesForResource(String resourceName) { + AssertUtil.assertNotBlank(resourceName, "resourceName cannot be blank"); + Set set = RULE_MAP.get(resourceName); + if (set == null) { + return new HashSet<>(); + } + return new HashSet<>(set); + } + + private static final class GatewayRulePropertyListener implements PropertyListener> { + + @Override + public void configUpdate(Set conf) { + applyGatewayRuleInternal(conf); + RecordLog.info("[GatewayRuleManager] Gateway flow rules received: " + RULE_MAP); + } + + @Override + public void configLoad(Set conf) { + applyGatewayRuleInternal(conf); + RecordLog.info("[GatewayRuleManager] Gateway flow rules loaded: " + RULE_MAP); + } + + private synchronized void applyGatewayRuleInternal(Set conf) { + if (conf == null || conf.isEmpty()) { + FlowRuleManager.loadRules(new ArrayList()); + ParamFlowRuleManager.loadRules(new ArrayList()); + RULE_MAP.clear(); + return; + } + Map> gatewayRuleMap = new ConcurrentHashMap<>(); + Map idxMap = new HashMap<>(); + List flowRules = new ArrayList<>(); + Set paramFlowRules = new HashSet<>(); + + for (GatewayFlowRule rule : conf) { + if (!isValidRule(rule)) { + RecordLog.warn("[GatewayRuleManager] Ignoring invalid rule when loading new rules: " + rule); + continue; + } + String resourceName = rule.getResource(); + if (rule.getParamItem() == null) { + // If param item is absent, it will be converted to normal flow rule. + flowRules.add(GatewayRuleConverter.toFlowRule(rule)); + } else { + // Prepare index map. + if (!idxMap.containsKey(resourceName)) { + idxMap.put(resourceName, 0); + } + int idx = idxMap.get(resourceName); + // Convert to parameter flow rule. + if (paramFlowRules.add(GatewayRuleConverter.applyToParamRule(rule, idx))) { + idxMap.put(rule.getResource(), idx + 1); + } + } + // Apply to the gateway rule map. + Set ruleSet = gatewayRuleMap.get(resourceName); + if (ruleSet == null) { + ruleSet = new HashSet<>(); + gatewayRuleMap.put(resourceName, ruleSet); + } + ruleSet.add(rule); + } + FlowRuleManager.loadRules(flowRules); + ParamFlowRuleManager.loadRules(new ArrayList<>(paramFlowRules)); + + RULE_MAP.clear(); + RULE_MAP.putAll(gatewayRuleMap); + } + } + + public static boolean isValidRule(GatewayFlowRule rule) { + if (rule == null || StringUtil.isBlank(rule.getResource()) || rule.getResourceMode() < 0 + || rule.getGrade() < 0 || rule.getCount() < 0 || rule.getBurst() < 0 || rule.getControlBehavior() < 0) { + return false; + } + if (rule.getGrade() == RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER + && rule.getMaxQueueingTimeoutMs() < 0) { + return false; + } + if (rule.getIntervalSec() <= 0) { + return false; + } + GatewayParamFlowItem item = rule.getParamItem(); + if (item != null) { + return isValidParamItem(item); + } + return true; + } + + static boolean isValidParamItem(/*@NonNull*/ GatewayParamFlowItem item) { + if (item.getParseStrategy() < 0) { + return false; + } + if (item.getParseStrategy() == SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM || + item.getParseStrategy() == SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HEADER) { + if (StringUtil.isBlank(item.getFieldName())) { + return false; + } + } + return StringUtil.isEmpty(item.getPattern()) || item.getMatchStrategy() >= 0; + } + + private GatewayRuleManager() {} +} diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/GatewayApiDefinitionManagerTest.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/GatewayApiDefinitionManagerTest.java new file mode 100644 index 00000000..0eed160e --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/api/GatewayApiDefinitionManagerTest.java @@ -0,0 +1,27 @@ +package com.alibaba.csp.sentinel.adapter.gateway.common.api; + +import java.util.Collections; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Eric Zhao + */ +public class GatewayApiDefinitionManagerTest { + + @Test + public void testIsValidApi() { + ApiDefinition bad1 = new ApiDefinition(); + ApiDefinition bad2 = new ApiDefinition("foo"); + ApiDefinition good1 = new ApiDefinition("foo") + .setPredicateItems(Collections.singleton(new ApiPathPredicateItem() + .setPattern("/abc") + )); + + assertFalse(GatewayApiDefinitionManager.isValidApi(bad1)); + assertFalse(GatewayApiDefinitionManager.isValidApi(bad2)); + assertTrue(GatewayApiDefinitionManager.isValidApi(good1)); + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleConverterTest.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleConverterTest.java new file mode 100644 index 00000000..0a9431a1 --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleConverterTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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 + * + * https://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.alibaba.csp.sentinel.adapter.gateway.common.rule; + +import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Eric Zhao + */ +public class GatewayRuleConverterTest { + + @Test + public void testConvertToFlowRule() { + GatewayFlowRule rule = new GatewayFlowRule("routeId1") + .setCount(10) + .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER) + .setMaxQueueingTimeoutMs(1000); + FlowRule flowRule = GatewayRuleConverter.toFlowRule(rule); + assertEquals(rule.getResource(), flowRule.getResource()); + assertEquals(rule.getCount(), flowRule.getCount(), 0.01); + assertEquals(rule.getControlBehavior(), flowRule.getControlBehavior()); + assertEquals(rule.getMaxQueueingTimeoutMs(), flowRule.getMaxQueueingTimeMs()); + } + + @Test + public void testConvertAndApplyToParamRule() { + GatewayFlowRule routeRule1 = new GatewayFlowRule("routeId1") + .setCount(2) + .setIntervalSec(2) + .setBurst(2) + .setParamItem(new GatewayParamFlowItem() + .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_CLIENT_IP) + ); + int idx = 1; + ParamFlowRule paramRule = GatewayRuleConverter.applyToParamRule(routeRule1, idx); + assertEquals(routeRule1.getResource(), paramRule.getResource()); + assertEquals(routeRule1.getCount(), paramRule.getCount(), 0.01); + assertEquals(routeRule1.getControlBehavior(), paramRule.getControlBehavior()); + assertEquals(routeRule1.getIntervalSec(), paramRule.getDurationInSec()); + assertEquals(routeRule1.getBurst(), paramRule.getBurstCount()); + assertEquals(idx, (int)paramRule.getParamIdx()); + assertEquals(idx, (int)routeRule1.getParamItem().getIndex()); + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleManagerTest.java b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleManagerTest.java new file mode 100644 index 00000000..ab1fcc15 --- /dev/null +++ b/sentinel-adapter/sentinel-api-gateway-adapter-common/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/common/rule/GatewayRuleManagerTest.java @@ -0,0 +1,115 @@ +/* + * Copyright 1999-2019 Alibaba Group Holding Ltd. + * + * 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 + * + * https://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.alibaba.csp.sentinel.adapter.gateway.common.rule; + +import java.util.HashSet; +import java.util.Set; + +import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Eric Zhao + */ +public class GatewayRuleManagerTest { + + @Test + public void testLoadAndGetGatewayRules() { + Set rules = new HashSet<>(); + String ahasRoute = "ahas_route"; + GatewayFlowRule rule1 = new GatewayFlowRule(ahasRoute) + .setCount(500) + .setIntervalSec(1); + GatewayFlowRule rule2 = new GatewayFlowRule(ahasRoute) + .setCount(20) + .setIntervalSec(2) + .setBurst(5) + .setParamItem(new GatewayParamFlowItem() + .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_CLIENT_IP) + ); + GatewayFlowRule rule3 = new GatewayFlowRule("complex_route_ZZZ") + .setCount(10) + .setIntervalSec(1) + .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER) + .setMaxQueueingTimeoutMs(600) + .setParamItem(new GatewayParamFlowItem() + .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HEADER) + .setFieldName("X-Sentinel-Flag") + ); + rules.add(rule1); + rules.add(rule2); + rules.add(rule3); + GatewayRuleManager.loadRules(rules); + + assertTrue(FlowRuleManager.hasConfig(ahasRoute)); + assertTrue(ParamFlowRuleManager.hasRules(ahasRoute)); + assertEquals(0, (int)rule2.getParamItem().getIndex()); + assertEquals(0, (int)rule3.getParamItem().getIndex()); + assertTrue(GatewayRuleManager.getRulesForResource(ahasRoute).contains(rule1)); + assertTrue(GatewayRuleManager.getRulesForResource(ahasRoute).contains(rule2)); + } + + @Test + public void testIsValidRule() { + GatewayFlowRule bad1 = new GatewayFlowRule(); + GatewayFlowRule bad2 = new GatewayFlowRule("abc") + .setIntervalSec(0); + GatewayFlowRule bad3 = new GatewayFlowRule("abc") + .setParamItem(new GatewayParamFlowItem() + .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM)); + GatewayFlowRule bad4 = new GatewayFlowRule("abc") + .setParamItem(new GatewayParamFlowItem() + .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM) + .setFieldName("p") + .setPattern("def") + .setMatchStrategy(-1) + ); + GatewayFlowRule good1 = new GatewayFlowRule("abc"); + GatewayFlowRule good2 = new GatewayFlowRule("abc") + .setParamItem(new GatewayParamFlowItem().setParseStrategy(0)); + GatewayFlowRule good3 = new GatewayFlowRule("abc") + .setParamItem(new GatewayParamFlowItem() + .setMatchStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HEADER) + .setFieldName("Origin") + .setPattern("def")); + assertFalse(GatewayRuleManager.isValidRule(bad1)); + assertFalse(GatewayRuleManager.isValidRule(bad2)); + assertFalse(GatewayRuleManager.isValidRule(bad3)); + assertFalse(GatewayRuleManager.isValidRule(bad4)); + + assertTrue(GatewayRuleManager.isValidRule(good1)); + assertTrue(GatewayRuleManager.isValidRule(good2)); + assertTrue(GatewayRuleManager.isValidRule(good3)); + } + + @Before + public void setUp() { + GatewayRuleManager.loadRules(new HashSet()); + } + + @After + public void tearDown() { + GatewayRuleManager.loadRules(new HashSet()); + } +} \ No newline at end of file