From f884269e051e45c51f3bd22d96b0320faa390234 Mon Sep 17 00:00:00 2001 From: ztzh_xieyun Date: Wed, 8 May 2024 19:10:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=BA=90=E7=A0=81=E8=B0=83=E6=95=B4=E9=99=90?= =?UTF-8?q?=E6=B5=81=E7=86=94=E6=96=AD=E3=80=81=E5=90=84=E7=A7=8D=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{GatewayInterface.java => ApiConfig.java} | 7 +- .../java/com/sf/vertx/api/pojo/AppConfig.java | 4 +- .../com/sf/vertx/api/pojo/SacService.java | 2 +- .../com/sf/vertx/api/pojo/VertxConfig.java | 6 +- sf-vertx/.settings/org.eclipse.jdt.core.prefs | 10 +- sf-vertx/pom.xml | 37 +- .../CircuitBreakerOptionsConverter.java | 101 ++++ .../com/sf/vertx/constans/RedisKeyConfig.java | 4 +- .../com/sf/vertx/constans/SacConstans.java | 7 + .../com/sf/vertx/constans/SacErrorCode.java | 51 ++ .../vertx/controller/AppConfigController.java | 34 +- .../com/sf/vertx/handle/AppConfigHandle.java | 293 +++++++++ .../com/sf/vertx/handle/BodyHandlerImpl.java | 16 +- .../vertx/handle/ParameterCheckHandler.java | 19 + .../handle/ParameterCheckHandlerImpl.java | 36 ++ .../com/sf/vertx/handle/ProxyHandler.java | 1 - .../com/sf/vertx/handle/ProxyHandlerImpl.java | 1 - .../sf/vertx/handle/ProxyModelUrlFuse.java | 135 +++++ .../handle/RateLimitHandlerRedisImpl.java | 72 +-- .../com/sf/vertx/handle/RedisRateLimiter.java | 32 +- .../handle/RestfulFailureHandlerImpl.java | 37 +- .../com/sf/vertx/init/DynamicBuildServer.java | 143 +---- .../sf/vertx/service/AppConfigService.java | 7 +- .../service/impl/AppConfigServiceImpl.java | 187 +----- .../java/com/sf/vertx/utils/ProxyTool.java | 47 +- .../java/examples/CircuitBreakerExamples.java | 257 ++++++++ .../examples/hystrix/HystrixExamples.java | 61 ++ .../java/examples/hystrix/package-info.java | 20 + .../vertx/circuitbreaker/CircuitBreaker.java | 257 ++++++++ .../circuitbreaker/CircuitBreakerOptions.java | 392 ++++++++++++ .../CircuitBreakerOptionsConverter.java | 101 ++++ .../circuitbreaker/CircuitBreakerState.java | 44 ++ .../vertx/circuitbreaker/FailurePolicy.java | 35 ++ .../HalfOpenCircuitException.java | 27 + .../circuitbreaker/HystrixMetricHandler.java | 50 ++ .../circuitbreaker/OpenCircuitException.java | 29 + .../io/vertx/circuitbreaker/RetryPolicy.java | 77 +++ .../circuitbreaker/TimeoutException.java | 29 + .../impl/CircuitBreakerImpl.java | 563 ++++++++++++++++++ .../impl/CircuitBreakerMetrics.java | 354 +++++++++++ .../impl/HystrixMetricEventStream.java | 139 +++++ .../io/vertx/circuitbreaker/package-info.java | 19 + .../impl/SharedClientHttpStreamEndpoint.java | 6 +- .../io/vertx/httpproxy/impl/ReverseProxy.java | 210 +++++-- .../test/java/com/sf/vertx/TestCaffeine.java | 33 + .../java/com/sf/vertx/TestCircuitBreaker.java | 4 +- 46 files changed, 3492 insertions(+), 504 deletions(-) rename sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/{GatewayInterface.java => ApiConfig.java} (55%) create mode 100644 sf-vertx/src/main/generated/io/vertx/circuitbreaker/CircuitBreakerOptionsConverter.java create mode 100644 sf-vertx/src/main/java/com/sf/vertx/constans/SacConstans.java create mode 100644 sf-vertx/src/main/java/com/sf/vertx/constans/SacErrorCode.java create mode 100644 sf-vertx/src/main/java/com/sf/vertx/handle/AppConfigHandle.java create mode 100644 sf-vertx/src/main/java/com/sf/vertx/handle/ParameterCheckHandler.java create mode 100644 sf-vertx/src/main/java/com/sf/vertx/handle/ParameterCheckHandlerImpl.java create mode 100644 sf-vertx/src/main/java/com/sf/vertx/handle/ProxyModelUrlFuse.java create mode 100644 sf-vertx/src/main/java/examples/CircuitBreakerExamples.java create mode 100644 sf-vertx/src/main/java/examples/hystrix/HystrixExamples.java create mode 100644 sf-vertx/src/main/java/examples/hystrix/package-info.java create mode 100644 sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreaker.java create mode 100644 sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreakerOptions.java create mode 100644 sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreakerOptionsConverter.java create mode 100644 sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreakerState.java create mode 100644 sf-vertx/src/main/java/io/vertx/circuitbreaker/FailurePolicy.java create mode 100644 sf-vertx/src/main/java/io/vertx/circuitbreaker/HalfOpenCircuitException.java create mode 100644 sf-vertx/src/main/java/io/vertx/circuitbreaker/HystrixMetricHandler.java create mode 100644 sf-vertx/src/main/java/io/vertx/circuitbreaker/OpenCircuitException.java create mode 100644 sf-vertx/src/main/java/io/vertx/circuitbreaker/RetryPolicy.java create mode 100644 sf-vertx/src/main/java/io/vertx/circuitbreaker/TimeoutException.java create mode 100644 sf-vertx/src/main/java/io/vertx/circuitbreaker/impl/CircuitBreakerImpl.java create mode 100644 sf-vertx/src/main/java/io/vertx/circuitbreaker/impl/CircuitBreakerMetrics.java create mode 100644 sf-vertx/src/main/java/io/vertx/circuitbreaker/impl/HystrixMetricEventStream.java create mode 100644 sf-vertx/src/main/java/io/vertx/circuitbreaker/package-info.java create mode 100644 sf-vertx/src/test/java/com/sf/vertx/TestCaffeine.java diff --git a/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/GatewayInterface.java b/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/ApiConfig.java similarity index 55% rename from sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/GatewayInterface.java rename to sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/ApiConfig.java index 31fcc99..c09f22e 100644 --- a/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/GatewayInterface.java +++ b/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/ApiConfig.java @@ -6,11 +6,10 @@ import java.util.List; import lombok.Data; @Data -public class GatewayInterface implements Serializable { - private static final long serialVersionUID = -313734935498432381L; +public class ApiConfig implements Serializable { + private static final long serialVersionUID = 5774283776114726263L; + private String apiCode; private String uri; - private boolean uriRegular; // uri 正则 private String method; // 大写 private List strategy; // 策略 - } diff --git a/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/AppConfig.java b/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/AppConfig.java index e2b3d4d..08dc8d3 100644 --- a/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/AppConfig.java +++ b/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/AppConfig.java @@ -10,8 +10,8 @@ public class AppConfig implements Serializable { private static final long serialVersionUID = 1518165296680157119L; private String appCode; // 应用唯一码, app访问uri添加前缀,用于网关区分多应用 private boolean exclusiveService; // 预留字段, 独立端口 - private Integer exclusiveGatewayConfigId; // 预留字段, 独享网关配置编号 - private EnvironmentConfig environmentConfig; // 环境配置 + private Integer exclusiveGatewayCode; // 预留字段, 独享网关配置编号 + //private EnvironmentConfig environmentConfig; // 环境配置 private List service; // 服务 private DataSecurity dataSecurity; // 数据加解密 private Strategy apiCurrentLimitingConfig; // 接口限流配置 diff --git a/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/SacService.java b/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/SacService.java index 86384a0..55b35a1 100644 --- a/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/SacService.java +++ b/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/SacService.java @@ -11,6 +11,6 @@ public class SacService implements Serializable { private String serviceName; // 服务名 private String serviceModel; // 模式, NORMAL, ROUTE private ServerAddress serverAddress; // NORMAL模式的服务地址 - private List uriList; // uri列表 + private List apiConfig; // request set header sacApiCode private Router routeConfig; // 路由 } diff --git a/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/VertxConfig.java b/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/VertxConfig.java index 09c2fb5..4532615 100644 --- a/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/VertxConfig.java +++ b/sf-vertx-api/src/main/java/com/sf/vertx/api/pojo/VertxConfig.java @@ -8,9 +8,9 @@ import lombok.Data; public class VertxConfig implements Serializable { private static final long serialVersionUID = -1706421732809219829L; private Integer port; // 启动端口 - private String appHeaderKey; - private String appHeaderServiceName; - private String rateLimitModel = "redis"; // 负载均衡模式 + private String appCodeHeaderKey = "sacAppCode"; + private String apiCodeHeaderKey = "sacApiCode"; + private String rateLimitModel = "redis"; // local,redis 负载均衡模式 private VertxOptionsConfig vertxOptionsConfig; private HttpClientOptionsConfig httpClientOptionsConfig; // 配置Vert端口连接池 private AddressRetryStrategy addressRetryStrategy; diff --git a/sf-vertx/.settings/org.eclipse.jdt.core.prefs b/sf-vertx/.settings/org.eclipse.jdt.core.prefs index 2f5cc74..3a0745f 100644 --- a/sf-vertx/.settings/org.eclipse.jdt.core.prefs +++ b/sf-vertx/.settings/org.eclipse.jdt.core.prefs @@ -1,8 +1,16 @@ eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.source=1.8 diff --git a/sf-vertx/pom.xml b/sf-vertx/pom.xml index d66dfb2..d0f36ef 100644 --- a/sf-vertx/pom.xml +++ b/sf-vertx/pom.xml @@ -14,6 +14,7 @@ 4.5.7 + 1.5.2 @@ -112,10 +113,10 @@ io.vertx vertx-web-client - + io.vertx vertx-junit5 @@ -174,7 +175,39 @@ io.netty netty-all + + + org.hdrhistogram + HdrHistogram + 2.1.12 + true + + + + com.netflix.hystrix + hystrix-core + ${hystrix.version} + provided + true + + + org.hdrhistogram + HdrHistogram + + + + + com.jayway.restassured + rest-assured + 2.8.0 + test + + + com.github.ben-manes.caffeine + caffeine + 2.6.2 + diff --git a/sf-vertx/src/main/generated/io/vertx/circuitbreaker/CircuitBreakerOptionsConverter.java b/sf-vertx/src/main/generated/io/vertx/circuitbreaker/CircuitBreakerOptionsConverter.java new file mode 100644 index 0000000..b2152c0 --- /dev/null +++ b/sf-vertx/src/main/generated/io/vertx/circuitbreaker/CircuitBreakerOptionsConverter.java @@ -0,0 +1,101 @@ +package io.vertx.circuitbreaker; + +import io.vertx.core.json.JsonObject; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.impl.JsonUtil; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Base64; + +/** + * Converter and mapper for {@link io.vertx.circuitbreaker.CircuitBreakerOptions}. + * NOTE: This class has been automatically generated from the {@link io.vertx.circuitbreaker.CircuitBreakerOptions} original class using Vert.x codegen. + */ +public class CircuitBreakerOptionsConverter { + + + private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER; + private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER; + + static void fromJson(Iterable> json, CircuitBreakerOptions obj) { + for (java.util.Map.Entry member : json) { + switch (member.getKey()) { + case "failuresRollingWindow": + if (member.getValue() instanceof Number) { + obj.setFailuresRollingWindow(((Number)member.getValue()).longValue()); + } + break; + case "fallbackOnFailure": + if (member.getValue() instanceof Boolean) { + obj.setFallbackOnFailure((Boolean)member.getValue()); + } + break; + case "maxFailures": + if (member.getValue() instanceof Number) { + obj.setMaxFailures(((Number)member.getValue()).intValue()); + } + break; + case "maxRetries": + if (member.getValue() instanceof Number) { + obj.setMaxRetries(((Number)member.getValue()).intValue()); + } + break; + case "metricsRollingBuckets": + if (member.getValue() instanceof Number) { + obj.setMetricsRollingBuckets(((Number)member.getValue()).intValue()); + } + break; + case "metricsRollingWindow": + if (member.getValue() instanceof Number) { + obj.setMetricsRollingWindow(((Number)member.getValue()).longValue()); + } + break; + case "notificationAddress": + if (member.getValue() instanceof String) { + obj.setNotificationAddress((String)member.getValue()); + } + break; + case "notificationLocalOnly": + if (member.getValue() instanceof Boolean) { + obj.setNotificationLocalOnly((Boolean)member.getValue()); + } + break; + case "notificationPeriod": + if (member.getValue() instanceof Number) { + obj.setNotificationPeriod(((Number)member.getValue()).longValue()); + } + break; + case "resetTimeout": + if (member.getValue() instanceof Number) { + obj.setResetTimeout(((Number)member.getValue()).longValue()); + } + break; + case "timeout": + if (member.getValue() instanceof Number) { + obj.setTimeout(((Number)member.getValue()).longValue()); + } + break; + } + } + } + + static void toJson(CircuitBreakerOptions obj, JsonObject json) { + toJson(obj, json.getMap()); + } + + static void toJson(CircuitBreakerOptions obj, java.util.Map json) { + json.put("failuresRollingWindow", obj.getFailuresRollingWindow()); + json.put("fallbackOnFailure", obj.isFallbackOnFailure()); + json.put("maxFailures", obj.getMaxFailures()); + json.put("maxRetries", obj.getMaxRetries()); + json.put("metricsRollingBuckets", obj.getMetricsRollingBuckets()); + json.put("metricsRollingWindow", obj.getMetricsRollingWindow()); + if (obj.getNotificationAddress() != null) { + json.put("notificationAddress", obj.getNotificationAddress()); + } + json.put("notificationLocalOnly", obj.isNotificationLocalOnly()); + json.put("notificationPeriod", obj.getNotificationPeriod()); + json.put("resetTimeout", obj.getResetTimeout()); + json.put("timeout", obj.getTimeout()); + } +} diff --git a/sf-vertx/src/main/java/com/sf/vertx/constans/RedisKeyConfig.java b/sf-vertx/src/main/java/com/sf/vertx/constans/RedisKeyConfig.java index 8a5985f..96388f2 100644 --- a/sf-vertx/src/main/java/com/sf/vertx/constans/RedisKeyConfig.java +++ b/sf-vertx/src/main/java/com/sf/vertx/constans/RedisKeyConfig.java @@ -10,9 +10,9 @@ public class RedisKeyConfig { private String vertxEnvironment; public static final String BASE_REDIS_KEY = "vertx:config:"; + public static String APP_CONFIG_PREFIX_KEY = null; public static String APP_CONFIG_SET_KEY = null; public static String APP_CURRENT_LIMITING_CONFIG_KEY = null; - public static String APP_CONFIG_PREFIX_KEY = null; public static String VERTX_CONFIG_STRING_KEY = null; public static String VERTX_ADDRESS_RETRY_STRATEGY_SET_KEY = null; public static String VERTX_ADDRESS_RETRY_STRATEGY_KEY = null; @@ -21,7 +21,7 @@ public class RedisKeyConfig { APP_CONFIG_PREFIX_KEY = BASE_REDIS_KEY + vertxEnvironment; APP_CONFIG_SET_KEY = APP_CONFIG_PREFIX_KEY + ":set"; APP_CURRENT_LIMITING_CONFIG_KEY = APP_CONFIG_PREFIX_KEY + ":app:limiting"; - VERTX_CONFIG_STRING_KEY = BASE_REDIS_KEY + vertxEnvironment + ":vertx"; + VERTX_CONFIG_STRING_KEY = APP_CONFIG_PREFIX_KEY + ":vertx"; VERTX_ADDRESS_RETRY_STRATEGY_KEY = APP_CONFIG_PREFIX_KEY + ":addAddressRetryStrategy"; VERTX_ADDRESS_RETRY_STRATEGY_SET_KEY = VERTX_ADDRESS_RETRY_STRATEGY_KEY + ":set"; } diff --git a/sf-vertx/src/main/java/com/sf/vertx/constans/SacConstans.java b/sf-vertx/src/main/java/com/sf/vertx/constans/SacConstans.java new file mode 100644 index 0000000..8601a1e --- /dev/null +++ b/sf-vertx/src/main/java/com/sf/vertx/constans/SacConstans.java @@ -0,0 +1,7 @@ +package com.sf.vertx.constans; + +public class SacConstans { + public static final String CACHE_KEY_CONNECTOR = ":"; + public static final String CIRCUIT_BREAKER = "CIRCUIT_BREAKER"; + +} diff --git a/sf-vertx/src/main/java/com/sf/vertx/constans/SacErrorCode.java b/sf-vertx/src/main/java/com/sf/vertx/constans/SacErrorCode.java new file mode 100644 index 0000000..e369596 --- /dev/null +++ b/sf-vertx/src/main/java/com/sf/vertx/constans/SacErrorCode.java @@ -0,0 +1,51 @@ +package com.sf.vertx.constans; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; + +import io.vertx.core.json.JsonObject; + +public class SacErrorCode { + public static final Integer DEFAULT_ERROR_CODE = 10000; + public final static Map _ERROR = new HashMap<>(); + static { + _ERROR.put(400, "Bad Request"); + _ERROR.put(401, "Unauthorized"); + _ERROR.put(403, "Forbidden"); + _ERROR.put(404, "Not Found"); + _ERROR.put(413, "Request Entity Too Large"); + _ERROR.put(415, "Unsupported Media Type"); + _ERROR.put(500, "Internal Server Error"); + _ERROR.put(502, "Bad Gateway"); + _ERROR.put(503, "Service Unavailable"); + _ERROR.put(504, "Gateway Timeout"); + _ERROR.put(504, "Gateway Timeout"); + _ERROR.put(10000, "服务请求失败,请稍后再试"); // 默认错误提示 + _ERROR.put(10001, "应用禁止访问,请联系管理员"); + _ERROR.put(10010, "无法找到路由地址"); + _ERROR.put(10011, "无法找到匹配的加解密算法"); + _ERROR.put(10012, "参数传递错误"); + _ERROR.put(10013, "无法匹配加解密、熔断代理模式"); + _ERROR.put(10014, "反向代理执行错误"); + _ERROR.put(10015, "请求url被限流"); + _ERROR.put(10016, "请求url被熔断"); + _ERROR.put(10017, "应用请求url被限流"); + _ERROR.put(10018, "apiCode与uri不匹配"); + }; + + public static JsonObject returnErrorMsg(Integer errorCode) { + JsonObject json = new JsonObject(); + String msg = _ERROR.get(errorCode); + if(StringUtils.isBlank(msg)) { + // default + json.put("code", DEFAULT_ERROR_CODE); + json.put("msg", _ERROR.get(DEFAULT_ERROR_CODE)); + } else { + json.put("code", errorCode); + json.put("msg", msg); + } + return json; + } +} diff --git a/sf-vertx/src/main/java/com/sf/vertx/controller/AppConfigController.java b/sf-vertx/src/main/java/com/sf/vertx/controller/AppConfigController.java index 6c438ec..e4a099f 100644 --- a/sf-vertx/src/main/java/com/sf/vertx/controller/AppConfigController.java +++ b/sf-vertx/src/main/java/com/sf/vertx/controller/AppConfigController.java @@ -1,15 +1,18 @@ package com.sf.vertx.controller; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.alibaba.fastjson2.JSONObject; import com.sf.vertx.api.pojo.AppConfig; import com.sf.vertx.api.pojo.VertxConfig; import com.sf.vertx.service.AppConfigService; +import io.vertx.core.json.JsonObject; import lombok.extern.slf4j.Slf4j; /*** @@ -19,22 +22,37 @@ import lombok.extern.slf4j.Slf4j; * */ @RestController -@RequestMapping("/vertx/config") -@Slf4j +@RequestMapping("/vertx") public class AppConfigController { @Autowired private AppConfigService appConfigService; - @PostMapping("/saveAppConfig") - public String addAppConfig(@RequestBody String appConfig) { + @PostMapping("/app/config") + public JSONObject addAppConfig(@RequestBody AppConfig appConfig) { appConfigService.saveAppConfig(appConfig); - return "success"; + JSONObject json = new JSONObject(); + json.put("code", 200); + json.put("msg", "success"); + return json; } - @PostMapping("/saveVertxConfig") - public String saveVertxConfig(@RequestBody VertxConfig vertxConfig) { + @DeleteMapping("/app/config") + public JSONObject deleteAppConfig(@RequestBody AppConfig appConfig) { + appConfigService.deleteAppConfig(appConfig); + JSONObject json = new JSONObject(); + json.put("code", 200); + json.put("msg", "success"); + return json; + } + + + @PostMapping("/vertx/config") + public JSONObject saveVertxConfig(@RequestBody VertxConfig vertxConfig) { appConfigService.saveVertxConfig(vertxConfig); - return "success"; + JSONObject json = new JSONObject(); + json.put("code", 200); + json.put("msg", "success"); + return json; } diff --git a/sf-vertx/src/main/java/com/sf/vertx/handle/AppConfigHandle.java b/sf-vertx/src/main/java/com/sf/vertx/handle/AppConfigHandle.java new file mode 100644 index 0000000..1e050fe --- /dev/null +++ b/sf-vertx/src/main/java/com/sf/vertx/handle/AppConfigHandle.java @@ -0,0 +1,293 @@ +package com.sf.vertx.handle; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.RedisTemplate; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.TypeReference; +import com.sf.vertx.api.pojo.ApiConfig; +import com.sf.vertx.api.pojo.AppConfig; +import com.sf.vertx.api.pojo.Node; +import com.sf.vertx.api.pojo.RouteContent; +import com.sf.vertx.api.pojo.SacService; +import com.sf.vertx.api.pojo.Strategy; +import com.sf.vertx.api.pojo.VertxConfig; +import com.sf.vertx.arithmetic.roundRobin.SacLoadBalancing; +import com.sf.vertx.constans.RedisKeyConfig; +import com.sf.vertx.utils.ProxyTool; + +import cn.hutool.core.collection.ConcurrentHashSet; +import io.vertx.circuitbreaker.CircuitBreaker; +import io.vertx.circuitbreaker.CircuitBreakerOptions; +import io.vertx.core.Vertx; +import io.vertx.core.VertxOptions; +import lombok.extern.slf4j.Slf4j; + +/*** + * vertx配置维护 + * + * @author xy + * + */ +@Slf4j +public class AppConfigHandle { + private static VertxConfig VERTX_CONFIG = new VertxConfig(); + public static Vertx VERTX; + public static CircuitBreaker CONNECTION_CIRCUIT_BREAKER; + // global cache app config + private static final ConcurrentHashMap CACHE_APP_CONFIG_MAP = new ConcurrentHashMap<>(); + // global app config appCode - Strategy + private static final ConcurrentHashMap GLOBAL_APP_CURRENT_LIMITING_CONFIG_MAP = new ConcurrentHashMap<>(); + // global api config appCode - Strategy + private static final ConcurrentHashMap GLOBAL_API_CURRENT_LIMITING_CONFIG_MAP = new ConcurrentHashMap<>(); + // appCode:apiCode:SacLoadBalancing + private static ConcurrentHashMap LOADBALANCING_MAP = new ConcurrentHashMap<>(); + // appCode:apiCode - ApiConfig + private static ConcurrentHashMap APICODE_CONFIG_MAP = new ConcurrentHashMap<>(); + // apiCode限流配置 appCode:apiCode - Strategy + private static ConcurrentHashMap APICODE_CONFIG_CURRENT_LIMITING_MAP = new ConcurrentHashMap<>(); + + // apiCode熔断配置 appCode:apiCode - CircuitBreaker + private static ConcurrentHashMap APICODE_CONFIG_CIRCUIT_BREAKER_MAP = new ConcurrentHashMap<>(); + + // 禁用appCode + private static ConcurrentHashSet DISABLED_APPCODE = new ConcurrentHashSet(); + + public static void addDisabledAppcode(String appCode) { + DISABLED_APPCODE.add(appCode); + } + + public static boolean isDisabledAppcode(String appCode) { + return DISABLED_APPCODE.contains(appCode); + } + + public static boolean appDataSecurity(String appCode) { + return CACHE_APP_CONFIG_MAP.get(appCode).getDataSecurity() != null ? true : false; + } + + public static Strategy getGlobalAppCurrentLimitingConfig(String appCode) { + return GLOBAL_APP_CURRENT_LIMITING_CONFIG_MAP.get(appCode); + } + + public static Strategy getGlobalApiCurrentLimitingConfig(String appCode) { + return GLOBAL_API_CURRENT_LIMITING_CONFIG_MAP.get(appCode); + } + + public static AppConfig getAppConfig(String appCode) { + return CACHE_APP_CONFIG_MAP.get(appCode); + } + + public static boolean isDataSecurity(String appCode) { + return CACHE_APP_CONFIG_MAP.get(appCode).getDataSecurity() != null ? true : false; + } + + public static boolean isApiCodeCircuitBreaker(String key) { + return APICODE_CONFIG_CIRCUIT_BREAKER_MAP.get(key) != null ? true : false; + } + + public static CircuitBreaker getApiCodeCircuitBreaker(String key) { + return APICODE_CONFIG_CIRCUIT_BREAKER_MAP.get(key); + } + + public static Strategy getApiCurrentLimiting(String key) { + return APICODE_CONFIG_CURRENT_LIMITING_MAP.get(key); + } + + public static VertxConfig getVertxConfig() { + return VERTX_CONFIG; + } + + public static String getAppCodeHeaderKey() { + return VERTX_CONFIG.getAppCodeHeaderKey(); + } + + public static String getApiCodeHeaderKey() { + return VERTX_CONFIG.getApiCodeHeaderKey(); + } + + public static SacLoadBalancing getLoadBalancing(String key) { + return LOADBALANCING_MAP.get(key); + } + + public static ApiConfig getApicodeConfigMap(String key) { + return APICODE_CONFIG_MAP.get(key); + } + + public static boolean isApicodeUri(String key, String uri) { + return StringUtils.equals(APICODE_CONFIG_MAP.get(key).getUri(), uri); + } + + /*** + * 从redis加载数据 + * + * @throws Exception + */ + public static void initAllAppConfig(RedisTemplate redisTemplate) throws Exception { + Set set = redisTemplate.opsForZSet().range(RedisKeyConfig.APP_CONFIG_SET_KEY, 0, -1); + for (String appCode : set) { + AppConfigHandle.initAppConfig(redisTemplate, appCode); + } + } + + /*** + * 加载vertx配置 + */ + public static void initVertxConfig(RedisTemplate redisTemplate) { + String vertxConfigKey = RedisKeyConfig.VERTX_CONFIG_STRING_KEY; + String vertxConfigValue = redisTemplate.opsForValue().get(vertxConfigKey); + if (StringUtils.isNotBlank(vertxConfigValue)) { + VERTX_CONFIG = JSONObject.parseObject(vertxConfigValue, VertxConfig.class); + } + } + + public static void initAppConfig(RedisTemplate redisTemplate, String appCode) { + String appCodeKey = RedisKeyConfig.APP_CONFIG_PREFIX_KEY + ":" + appCode; + String appCodeValue = redisTemplate.opsForValue().get(appCodeKey); + if (StringUtils.isNotBlank(appCodeValue)) { + AppConfig appConfig = JSON.parseObject(appCodeValue, new TypeReference() { + }); + CACHE_APP_CONFIG_MAP.put(appCode, appConfig); + + // app、api默认限流 + if (appConfig.getApiCurrentLimitingConfig() != null) { + GLOBAL_API_CURRENT_LIMITING_CONFIG_MAP.put(appCode, appConfig.getApiCurrentLimitingConfig()); + + } + + if (appConfig.getAppCurrentLimitingConfig() != null) { + GLOBAL_APP_CURRENT_LIMITING_CONFIG_MAP.put(appCode, appConfig.getAppCurrentLimitingConfig()); + } + + // app router负载均衡 + for (SacService sacService : appConfig.getService()) { + List nodeList = new ArrayList<>(); + // 获取service模式 + if (StringUtils.equals(sacService.getServiceModel(), "NORMAL")) { + Node node = new Node(); + node.setIp(sacService.getServerAddress().getHost()); + node.setPort(sacService.getServerAddress().getPort()); + node.setWeight(0); + node.setProtocol(sacService.getServerAddress().getProtocol()); + nodeList.add(node); + } else if (StringUtils.equals(sacService.getServiceModel(), "ROUTE")) { + if (sacService.getRouteConfig() != null + && StringUtils.equals(sacService.getRouteConfig().getRouteType(), "WEIGHT_ROUTE")) { + for (RouteContent routeContent : sacService.getRouteConfig().getRouteContent()) { + Node node = new Node(); + node.setIp(routeContent.getServerAddress().getHost()); + node.setPort(routeContent.getServerAddress().getPort()); + node.setWeight(routeContent.getWeight() != null && routeContent.getWeight() > 0 + ? routeContent.getWeight() + : 0); + node.setProtocol(routeContent.getServerAddress().getProtocol()); + nodeList.add(node); + } + } + } + + // 初始化apiConfig + if (sacService.getApiConfig() != null && sacService.getApiConfig().size() > 0) { + for (ApiConfig apiConfig : sacService.getApiConfig()) { + String key = appCode + ":" + apiConfig.getApiCode(); + APICODE_CONFIG_MAP.put(key, apiConfig); + if (nodeList.size() > 0) { + // 初始化负载均衡算法 + SacLoadBalancing sacLoadBalancing = ProxyTool.roundRobin(nodeList); + LOADBALANCING_MAP.put(key, sacLoadBalancing); + } + + if (apiConfig.getStrategy() != null && apiConfig.getStrategy().size() > 0) { + for (Strategy strategy : apiConfig.getStrategy()) { + if (StringUtils.equals(strategy.getType(), "CURRENT_LIMITING")) { + APICODE_CONFIG_CURRENT_LIMITING_MAP.put(key, strategy); + } else if (StringUtils.equals(strategy.getType(), "CIRCUIT_BREAKER")) { + String keyCircuitBreaker = key + ":" + "CIRCUIT_BREAKER"; +// interfaceBreaker = CircuitBreaker.create("interfaceBreaker", VERTX, +// new CircuitBreakerOptions().setMaxFailures(3).setMaxRetries(5).setTimeout(2000) +// .setFallbackOnFailure(true) +// ).openHandler(v -> { +// log.info("Circuit opened"); +// }).closeHandler(v -> { +// log.info("Circuit closed"); +// });//.retryPolicy(retryCount -> retryCount * 100L); + + // apiCode熔断 + CircuitBreaker circuitBreaker = CircuitBreaker.create(keyCircuitBreaker + "-circuit-breaker", + VERTX, new CircuitBreakerOptions().setMaxFailures(strategy.getThreshold()) // 最大失败数 + .setFailuresRollingWindow(strategy.getTimeWindow() * 1000) // 毫秒 + // .setTimeout(2000) // 超时时间 + .setFallbackOnFailure(true) // 失败后是否调用回退函数(fallback) + .setResetTimeout(strategy.getRecovery_interval()) // 在开启状态下,尝试重试之前所需时间 + ).openHandler(v -> { + log.info(keyCircuitBreaker + " Circuit open"); + }).halfOpenHandler(v -> { + log.info(keyCircuitBreaker + "Circuit halfOpen"); + }).closeHandler(v -> { + log.info(keyCircuitBreaker + "Circuit close"); + }); + APICODE_CONFIG_CIRCUIT_BREAKER_MAP.put(keyCircuitBreaker, circuitBreaker); + } + } + } + + } + } + + } + } + } + + public static Vertx createVertx() { + // TODO 编解码线程池,后面优化协程等方式 + VertxOptions vertxOptions = new VertxOptions(); + loadVertxOptions(vertxOptions); + VERTX = Vertx.vertx(vertxOptions); + + initConnectionCircuitBreaker(); + return VERTX; + } + + /*** + * 初始化connection Breaker + */ + private static void initConnectionCircuitBreaker() { + CONNECTION_CIRCUIT_BREAKER = CircuitBreaker.create("connectionCircuitBreaker-circuit-breaker", VERTX, + new CircuitBreakerOptions().setMaxFailures(3) // 最大失败数 + .setTimeout(2000) // 超时时间 + .setFallbackOnFailure(true) // 失败后是否调用回退函数(fallback) + .setResetTimeout(10000) // 在开启状态下,尝试重试之前所需时间 + ).openHandler(v -> { + log.info("connectionCircuitBreaker Circuit open"); + }).halfOpenHandler(v -> { + log.info("connectionCircuitBreaker Circuit halfOpen"); + }).closeHandler(v -> { + log.info("connectionCircuitBreaker Circuit close"); + }); + } + + + private static void loadVertxOptions(VertxOptions vertxOptions) { + long blockedThreadCheckInterval = VERTX_CONFIG.getVertxOptionsConfig() == null ? -1 + : VERTX_CONFIG.getVertxOptionsConfig().getBlockedThreadCheckInterval(); + int workerPoolSize = VERTX_CONFIG == null || VERTX_CONFIG.getVertxOptionsConfig() == null ? -1 + : VERTX_CONFIG.getVertxOptionsConfig().getWorkerPoolSize(); + if (workerPoolSize != -1) { + vertxOptions.setWorkerPoolSize(workerPoolSize); + } + + // TODO + blockedThreadCheckInterval = 1000000L; + if (blockedThreadCheckInterval != -1) { + vertxOptions.setBlockedThreadCheckInterval(blockedThreadCheckInterval); // 不打印Thread blocked 阻塞日志 + } + } + + + +} diff --git a/sf-vertx/src/main/java/com/sf/vertx/handle/BodyHandlerImpl.java b/sf-vertx/src/main/java/com/sf/vertx/handle/BodyHandlerImpl.java index 9133f41..d9817e0 100644 --- a/sf-vertx/src/main/java/com/sf/vertx/handle/BodyHandlerImpl.java +++ b/sf-vertx/src/main/java/com/sf/vertx/handle/BodyHandlerImpl.java @@ -5,11 +5,6 @@ import java.util.List; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; -import org.apache.commons.lang3.StringUtils; - -import com.sf.vertx.service.impl.AppConfigServiceImpl; -import com.sf.vertx.utils.ProxyTool; - /* * Copyright 2014 Red Hat, Inc. * @@ -80,9 +75,14 @@ public class BodyHandlerImpl implements BodyHandler { // TODO 改造了这个地方 final HttpServerRequest request = context.request(); final HttpServerResponse response = context.response(); - String sacAppHeaderKey = request.getHeader(AppConfigServiceImpl.getSacAppHeaderKey()); - if (StringUtils.isNotBlank(sacAppHeaderKey) && AppConfigServiceImpl.appDataSecurity(sacAppHeaderKey)) { - // 加解密在proxy拦截器解析跳转 + String appCode = context.request().headers().get(AppConfigHandle.getAppCodeHeaderKey()); + String apiCode = context.request().headers().get(AppConfigHandle.getApiCodeHeaderKey()); + // 判断
+ // 1、是否配置全局加解密.
+ // 2、apiCode 配置熔断 + String keyCircuitBreaker = appCode + ":" + apiCode + ":" + "CIRCUIT_BREAKER"; + if (AppConfigHandle.isDataSecurity(appCode) + || AppConfigHandle.isApiCodeCircuitBreaker(keyCircuitBreaker)) { // =======源码流程 // final HttpServerRequest request = context.request(); // final HttpServerResponse response = context.response(); diff --git a/sf-vertx/src/main/java/com/sf/vertx/handle/ParameterCheckHandler.java b/sf-vertx/src/main/java/com/sf/vertx/handle/ParameterCheckHandler.java new file mode 100644 index 0000000..ce62ab5 --- /dev/null +++ b/sf-vertx/src/main/java/com/sf/vertx/handle/ParameterCheckHandler.java @@ -0,0 +1,19 @@ +package com.sf.vertx.handle; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/*** + * 入口参数校验 + * + * @author xy + * + */ +@VertxGen +public interface ParameterCheckHandler extends Handler { + + static ParameterCheckHandler create() { + return new ParameterCheckHandlerImpl(); + } +} diff --git a/sf-vertx/src/main/java/com/sf/vertx/handle/ParameterCheckHandlerImpl.java b/sf-vertx/src/main/java/com/sf/vertx/handle/ParameterCheckHandlerImpl.java new file mode 100644 index 0000000..1f1f2ca --- /dev/null +++ b/sf-vertx/src/main/java/com/sf/vertx/handle/ParameterCheckHandlerImpl.java @@ -0,0 +1,36 @@ +package com.sf.vertx.handle; + +import org.apache.commons.lang3.StringUtils; + +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.HttpException; + +public class ParameterCheckHandlerImpl implements ParameterCheckHandler { + + @Override + public void handle(RoutingContext rc) { + String appCode = rc.request().headers().get(AppConfigHandle.getAppCodeHeaderKey()); + String apiCode = rc.request().headers().get(AppConfigHandle.getApiCodeHeaderKey()); + String key = appCode + ":" + apiCode; + if (StringUtils.isBlank(appCode) || StringUtils.isBlank(apiCode) + || AppConfigHandle.getAppConfig(appCode) == null + || AppConfigHandle.getApicodeConfigMap(key) == null) { + rc.fail(new HttpException(10012)); + return; + } + + if(AppConfigHandle.isDisabledAppcode(apiCode)) { + rc.fail(new HttpException(10001)); + return; + } + + String uri = rc.request().uri(); + if(AppConfigHandle.isApicodeUri(key, uri) == false) { + rc.fail(new HttpException(10018)); + return; + } + + rc.next(); + return; + } +} diff --git a/sf-vertx/src/main/java/com/sf/vertx/handle/ProxyHandler.java b/sf-vertx/src/main/java/com/sf/vertx/handle/ProxyHandler.java index 8022108..44cd8a5 100644 --- a/sf-vertx/src/main/java/com/sf/vertx/handle/ProxyHandler.java +++ b/sf-vertx/src/main/java/com/sf/vertx/handle/ProxyHandler.java @@ -1,6 +1,5 @@ package com.sf.vertx.handle; -import io.vertx.circuitbreaker.CircuitBreaker; import io.vertx.codegen.annotations.VertxGen; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; diff --git a/sf-vertx/src/main/java/com/sf/vertx/handle/ProxyHandlerImpl.java b/sf-vertx/src/main/java/com/sf/vertx/handle/ProxyHandlerImpl.java index 4221167..c52f915 100644 --- a/sf-vertx/src/main/java/com/sf/vertx/handle/ProxyHandlerImpl.java +++ b/sf-vertx/src/main/java/com/sf/vertx/handle/ProxyHandlerImpl.java @@ -1,6 +1,5 @@ package com.sf.vertx.handle; -import io.vertx.circuitbreaker.CircuitBreaker; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.client.WebClient; import io.vertx.httpproxy.HttpProxy; diff --git a/sf-vertx/src/main/java/com/sf/vertx/handle/ProxyModelUrlFuse.java b/sf-vertx/src/main/java/com/sf/vertx/handle/ProxyModelUrlFuse.java new file mode 100644 index 0000000..2a6c6cd --- /dev/null +++ b/sf-vertx/src/main/java/com/sf/vertx/handle/ProxyModelUrlFuse.java @@ -0,0 +1,135 @@ +//package com.sf.vertx.handle; +// +//import java.util.HashMap; +//import java.util.Map; +// +//import org.apache.commons.lang3.StringUtils; +// +//import com.sf.vertx.api.pojo.GatewayInterface; +//import com.sf.vertx.api.pojo.SacService; +//import com.sf.vertx.api.pojo.Strategy; +//import com.sf.vertx.constans.SacConstans; +//import com.sf.vertx.service.impl.AppConfigServiceImpl; +//import com.sf.vertx.utils.ProxyTool; +// +//import io.vertx.circuitbreaker.CircuitBreaker; +//import io.vertx.httpproxy.ProxyContext; +//import lombok.extern.slf4j.Slf4j; +// +//@Slf4j +//public class ProxyModelUrlFuse { +// +// private static Map match(ProxyContext context) { +// Map match = new HashMap<>(); +// Strategy strategyFuse = null; // 接口 +// String sacAppHeaderKey = context.request().headers().get(AppConfigServiceImpl.getSacAppHeaderKey()); +// String serviceName = context.request().headers().get(AppConfigServiceImpl.getAppHeaderServiceName()); +// +// SacService sacService = AppConfigServiceImpl.getAppService(sacAppHeaderKey, serviceName); +// boolean matchUri = false; +// GatewayInterface _gatewayInterface = null; +// if (sacService.getUriList() != null && sacService.getUriList().size() > 0) { +// for (GatewayInterface gatewayInterface : sacService.getUriList()) { +// // uri匹配 +// if (gatewayInterface.isUriRegular()) { +// if (ProxyTool.regexMatch(gatewayInterface.getUri(), context.request().getURI())) { +// matchUri = true; +// } +// } else { +// if (StringUtils.equals(gatewayInterface.getUri(), context.request().getURI())) { +// matchUri = true; +// } +// } +// +// if (matchUri) { +// if (gatewayInterface.getStrategy() != null && gatewayInterface.getStrategy().size() > 0) { +// for (Strategy strategy : gatewayInterface.getStrategy()) { +// if (StringUtils.equals(strategy.getType(), SacConstans.CIRCUIT_BREAKER)) { +// strategyFuse = strategy; +// _gatewayInterface = gatewayInterface; +// break; +// } +// } +// break; +// } +// } +// } +// if (strategyFuse != null) { +// match.put("strategyFuse", strategyFuse); +// } +// +// if (_gatewayInterface != null) { +// match.put("gatewayInterface", _gatewayInterface); +// } +// } +// return match; +// } +// +// private static String getBreakerName(ProxyContext context) { +// Map match = match(context); +// if (match.get("strategyFuse") != null) { +// GatewayInterface gatewayInterface = (GatewayInterface)match.get("gatewayInterface"); +// String sacAppHeaderKey = context.request().headers().get(AppConfigServiceImpl.getSacAppHeaderKey()); +// String serviceName = context.request().headers().get(AppConfigServiceImpl.getAppHeaderServiceName()); +// String breakerName = sacAppHeaderKey + SacConstans.CACHE_KEY_CONNECTOR + serviceName +// + SacConstans.CACHE_KEY_CONNECTOR + gatewayInterface.getUri() + SacConstans.CACHE_KEY_CONNECTOR +// + gatewayInterface.getMethod(); +// +// } +// return null; +// } +// +// /*** +// * 接口熔断校验 +// * +// * @param context +// */ +// public static String validateFuse(ProxyContext context) { +// Map match = match(context); +// if (match.get("strategyFuse") != null) { +// GatewayInterface _gatewayInterface = (GatewayInterface)match.get("gatewayInterface"); +// String sacAppHeaderKey = context.request().headers().get(AppConfigServiceImpl.getSacAppHeaderKey()); +// String serviceName = context.request().headers().get(AppConfigServiceImpl.getAppHeaderServiceName()); +// GatewayInterface gatewayInterface = (GatewayInterface)match.get("gatewayInterface"); +// String breakerName = sacAppHeaderKey + SacConstans.CACHE_KEY_CONNECTOR + serviceName +// + SacConstans.CACHE_KEY_CONNECTOR + _gatewayInterface.getUri() + SacConstans.CACHE_KEY_CONNECTOR +// + _gatewayInterface.getMethod(); +// CircuitBreaker circuitBreaker = AppConfigServiceImpl.getCacheApiBreakerConfig(breakerName); +// circuitBreaker.executeWithFallback(promise -> { +// if (context.response().getStatusCode() == 200) { +// promise.complete("1"); +// } else { +// promise.fail("2"); +// } +// }, v -> { +// // Executed when the circuit is opened +// log.info(breakerName + " executed when the circuit is opened:{}", v); +// return "3"; +// }, ar -> { +// // Do something with the result +// log.info(breakerName + " interface result.{} ", ar); +// }); +// } +// return match.get("strategyFuse") == null ? null : ((Strategy) match.get("strategyFuse")).getDefaultResponse(); +// } +// +//// private void add(String rateLimitModel, RedisTemplate redisTemplate, String key) { +//// +//// // 新增 +//// +//// String limitKey = sacAppHeaderKey + ";" + serviceName + ";" + context.request().getMethod().name() + ";" +//// + context.request().getURI(); +//// ThreadUtil.execAsync(() -> { +//// add(rateLimitModel, limitKey); +//// }); +//// Buffer buffer = Buffer.buffer("Gateway Interface return error!"); +//// context.response().setBody(Body.body(buffer)); +//// +//// // log.info("redis app threshold: {}", redisTemplate.opsForValue().get(key)); +//// Long incr = redisTemplate.opsForValue().increment(key); +//// if (incr == 1) { // 创建,才设置时间窗口 +//// redisTemplate.expire(key, timeWindow, TimeUnit.SECONDS); +//// } +//// } +// +//} diff --git a/sf-vertx/src/main/java/com/sf/vertx/handle/RateLimitHandlerRedisImpl.java b/sf-vertx/src/main/java/com/sf/vertx/handle/RateLimitHandlerRedisImpl.java index 05ea50d..4f7a9a2 100644 --- a/sf-vertx/src/main/java/com/sf/vertx/handle/RateLimitHandlerRedisImpl.java +++ b/sf-vertx/src/main/java/com/sf/vertx/handle/RateLimitHandlerRedisImpl.java @@ -2,12 +2,7 @@ package com.sf.vertx.handle; import java.util.Map; -import org.apache.commons.lang3.StringUtils; -import com.sf.vertx.api.pojo.GatewayInterface; -import com.sf.vertx.api.pojo.SacService; import com.sf.vertx.api.pojo.Strategy; -import com.sf.vertx.service.impl.AppConfigServiceImpl; -import com.sf.vertx.utils.ProxyTool; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.HttpException; @@ -30,67 +25,30 @@ public class RateLimitHandlerRedisImpl implements RateLimitHandler { @Override public void handle(RoutingContext rc) { - String sacAppHeaderKey = rc.request().headers().get(AppConfigServiceImpl.getSacAppHeaderKey()); - String appHeaderServiceName = rc.request().getHeader(AppConfigServiceImpl.getAppHeaderServiceName()); - if (StringUtils.isBlank(sacAppHeaderKey) || StringUtils.isBlank(appHeaderServiceName)) { // 参数传递错误 - rc.fail(new HttpException(10003)); - return; - } - // 查询模式 - SacService sacService = AppConfigServiceImpl.getSacService(sacAppHeaderKey, appHeaderServiceName); - if (sacService == null) { // 参数传递错误 - rc.fail(new HttpException(10003)); - return; - } - boolean matchUri = false; - Strategy strategyInterface = null; // 接口 - if (sacService.getUriList() != null) { - for (GatewayInterface uri : sacService.getUriList()) { - if (uri.isUriRegular()) { - if (ProxyTool.regexMatch(uri.getUri(), rc.request().uri())) { - matchUri = true; - } - } else { - if (StringUtils.equals(uri.getUri(), rc.request().uri())) { - matchUri = true; - } - } - if (matchUri) { - for (Strategy strategy : uri.getStrategy()) { - if (StringUtils.equals(strategy.getType(), "CURRENT_LIMITING")) { - strategyInterface = strategy; - } - } - break; - } - } - } + String appCodeHeaderKey = rc.request().headers().get(AppConfigHandle.getAppCodeHeaderKey()); + String apiCodeHeaderKey = rc.request().headers().get(AppConfigHandle.getApiCodeHeaderKey()); + Strategy apiCodeStrategy = AppConfigHandle.getApiCurrentLimiting(appCodeHeaderKey + ":" + apiCodeHeaderKey); - strategyInterface = strategyInterface != null ? strategyInterface - : (AppConfigServiceImpl.getAppConfig(sacAppHeaderKey) != null - && AppConfigServiceImpl.getAppConfig(sacAppHeaderKey).getApiCurrentLimitingConfig() != null - ? AppConfigServiceImpl.getAppConfig(sacAppHeaderKey).getApiCurrentLimitingConfig() - : null); + Strategy globalApiStrategy = AppConfigHandle.getGlobalApiCurrentLimitingConfig(appCodeHeaderKey); + Strategy globalAppStrategy = AppConfigHandle.getGlobalAppCurrentLimitingConfig(appCodeHeaderKey); - Strategy strategyApp = AppConfigServiceImpl.getAppConfig(sacAppHeaderKey) != null - && AppConfigServiceImpl.getAppConfig(sacAppHeaderKey).getAppCurrentLimitingConfig() != null - ? AppConfigServiceImpl.getAppConfig(sacAppHeaderKey).getAppCurrentLimitingConfig() - : null; - if (strategyInterface != null || strategyApp != null) { - Map retMap = rateLimiter.acquire(rc, strategyInterface, strategyApp); + Strategy apiStrategy = apiCodeStrategy != null ? apiCodeStrategy + : globalApiStrategy != null ? globalApiStrategy : null; + Strategy appStrategy = globalAppStrategy != null ? globalAppStrategy : null; + + if (apiStrategy != null || appStrategy != null) { + Map retMap = rateLimiter.acquire(rc, apiStrategy, appStrategy); - if (strategyInterface != null && retMap.get(1) == false) { - rc.fail(new HttpException(10101, strategyInterface.getDefaultResponse())); + if (apiStrategy != null && retMap.get(1) == false) { + rc.fail(new HttpException(10015, apiStrategy.getDefaultResponse())); return; } - if (strategyApp != null && retMap.get(2) == false) { - rc.fail(new HttpException(10102, strategyApp.getDefaultResponse())); + if (appStrategy != null && retMap.get(2) == false) { + rc.fail(new HttpException(10017, appStrategy.getDefaultResponse())); return; } } - - log.info("rateLimiter.acquire true"); rc.next(); return; } diff --git a/sf-vertx/src/main/java/com/sf/vertx/handle/RedisRateLimiter.java b/sf-vertx/src/main/java/com/sf/vertx/handle/RedisRateLimiter.java index e573494..e8c17db 100644 --- a/sf-vertx/src/main/java/com/sf/vertx/handle/RedisRateLimiter.java +++ b/sf-vertx/src/main/java/com/sf/vertx/handle/RedisRateLimiter.java @@ -8,7 +8,6 @@ import org.springframework.data.redis.core.RedisTemplate; import com.sf.vertx.api.pojo.Strategy; import com.sf.vertx.constans.RedisKeyConfig; -import com.sf.vertx.service.impl.AppConfigServiceImpl; import com.sf.vertx.utils.SpringUtils; import cn.hutool.core.thread.ThreadUtil; @@ -17,20 +16,20 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class RedisRateLimiter { - public Map acquire(RoutingContext rc, Strategy strategyInterface, Strategy strategyApp) { - String appCode = rc.request().headers().get(AppConfigServiceImpl.getSacAppHeaderKey()); + public Map acquire(RoutingContext rc, Strategy apiStrategy, Strategy appStrategy) { + String appCodeHeaderKey = rc.request().headers().get(AppConfigHandle.getAppCodeHeaderKey()); String key = null; Map retMap = new HashMap<>(); - if (strategyInterface != null) { - key = RedisKeyConfig.APP_CURRENT_LIMITING_CONFIG_KEY + ":" + appCode + ":" + rc.request().uri() + ":" - + rc.request().method(); - Boolean ret = rateLimiter(key, appCode, strategyInterface.getThreshold(), strategyInterface.getTimeWindow()); + if (apiStrategy != null) { + key = RedisKeyConfig.APP_CURRENT_LIMITING_CONFIG_KEY + ":" + appCodeHeaderKey + ":" + rc.request().uri() + + ":" + rc.request().method(); + Boolean ret = rateLimiter(key, appCodeHeaderKey, apiStrategy.getThreshold(), apiStrategy.getTimeWindow()); retMap.put(1, ret); } - if (strategyApp != null) { - key = RedisKeyConfig.APP_CURRENT_LIMITING_CONFIG_KEY + ":" + appCode; - Boolean ret = rateLimiter(key, appCode, strategyApp.getThreshold(), strategyApp.getTimeWindow()); + if (appStrategy != null) { + key = RedisKeyConfig.APP_CURRENT_LIMITING_CONFIG_KEY + ":" + appCodeHeaderKey; + Boolean ret = rateLimiter(key, appCodeHeaderKey, appStrategy.getThreshold(), appStrategy.getTimeWindow()); retMap.put(2, ret); } return retMap; @@ -39,18 +38,17 @@ public class RedisRateLimiter { @SuppressWarnings("unchecked") private Boolean rateLimiter(String key, String appCode, Integer threshold, Integer timeWindow) { RedisTemplate redisTemplate = SpringUtils.getBean("redisTemplate", RedisTemplate.class); - Object count = redisTemplate.opsForValue().get(key); + Integer count = redisTemplate.opsForValue().get(key); // redisTemplate.delete(key); - log.info("count:{}", count); + log.info("redis limiter. key:{}, count:{}", key, count); // 初始化,设置过期时间 ThreadUtil.execAsync(() -> { - add(timeWindow, redisTemplate, key); + increment(timeWindow, redisTemplate, key); }); - return count != null && Integer.valueOf(count.toString()) <= threshold ? true : false; + return (count == null || count <= threshold) ? true : false; } - - private void add(Integer timeWindow, RedisTemplate redisTemplate, String key) { - // log.info("redis app threshold: {}", redisTemplate.opsForValue().get(key)); + + private void increment(Integer timeWindow, RedisTemplate redisTemplate, String key) { Long incr = redisTemplate.opsForValue().increment(key); if (incr == 1) { // 创建,才设置时间窗口 redisTemplate.expire(key, timeWindow, TimeUnit.SECONDS); diff --git a/sf-vertx/src/main/java/com/sf/vertx/handle/RestfulFailureHandlerImpl.java b/sf-vertx/src/main/java/com/sf/vertx/handle/RestfulFailureHandlerImpl.java index 317b41e..fd8ee54 100644 --- a/sf-vertx/src/main/java/com/sf/vertx/handle/RestfulFailureHandlerImpl.java +++ b/sf-vertx/src/main/java/com/sf/vertx/handle/RestfulFailureHandlerImpl.java @@ -1,5 +1,8 @@ package com.sf.vertx.handle; +import org.apache.commons.lang3.StringUtils; + +import com.sf.vertx.constans.SacErrorCode; import com.sf.vertx.utils.ProxyTool; import io.vertx.core.http.HttpHeaders; @@ -7,28 +10,32 @@ import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.HttpException; import lombok.extern.slf4j.Slf4j; + @Slf4j public class RestfulFailureHandlerImpl implements RestfulFailureHandler { - @SuppressWarnings("unlikely-arg-type") @Override public void handle(RoutingContext frc) { - Throwable failure = frc.failure(); - log.info("Throwable error:{}", failure); - if (failure instanceof HttpException) { - HttpException httpException = (HttpException) failure; - // frc.response().setStatusCode(404).end(); - if(ProxyTool._ERROR.containsKey(httpException.getPayload())) { - JsonObject dataJson = new JsonObject(httpException.getPayload()); - frc.response().setChunked(true).setStatusCode(httpException.getStatusCode()) - .putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(dataJson.size())) - .putHeader("Content-Type", "application/json").end(dataJson.toBuffer()); - return; + JsonObject errorJson = null; + try { + Throwable failure = frc.failure(); + if (failure instanceof HttpException) { + HttpException httpException = (HttpException) failure; + if (StringUtils.isNoneBlank(httpException.getPayload())) { + errorJson = new JsonObject(httpException.getPayload()); + } else { + errorJson = SacErrorCode.returnErrorMsg(httpException.getStatusCode()); + } + } else { + errorJson = SacErrorCode.returnErrorMsg(SacErrorCode.DEFAULT_ERROR_CODE); } + } catch (Exception e) { + e.printStackTrace(); + errorJson = SacErrorCode.returnErrorMsg(SacErrorCode.DEFAULT_ERROR_CODE); } - frc.response().setChunked(true).setStatusCode(400).putHeader("Content-Type", "application/json") - .putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(ProxyTool.DEFAULT_ERROR_MSG.length())) - .end(ProxyTool.DEFAULT_ERROR_MSG); + + frc.response().setChunked(true).setStatusCode(500).putHeader("Content-Type", "application/json") + .putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(errorJson.size())).end(errorJson.toBuffer()); return; } diff --git a/sf-vertx/src/main/java/com/sf/vertx/init/DynamicBuildServer.java b/sf-vertx/src/main/java/com/sf/vertx/init/DynamicBuildServer.java index 456954b..a90bf63 100644 --- a/sf-vertx/src/main/java/com/sf/vertx/init/DynamicBuildServer.java +++ b/sf-vertx/src/main/java/com/sf/vertx/init/DynamicBuildServer.java @@ -1,6 +1,5 @@ package com.sf.vertx.init; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.ApplicationArguments; @@ -11,31 +10,21 @@ import org.springframework.stereotype.Component; import com.sf.vertx.api.pojo.VertxConfig; import com.sf.vertx.constans.RedisKeyConfig; +import com.sf.vertx.handle.AppConfigHandle; import com.sf.vertx.handle.BodyHandler; +import com.sf.vertx.handle.ParameterCheckHandler; import com.sf.vertx.handle.ProxyHandler; import com.sf.vertx.handle.RateLimitHandler; import com.sf.vertx.handle.RestfulFailureHandler; -import com.sf.vertx.service.AppConfigService; -import com.sf.vertx.service.impl.AppConfigServiceImpl; -import com.sf.vertx.utils.Filter; import com.sf.vertx.utils.ProxyTool; -import io.vertx.circuitbreaker.CircuitBreaker; -import io.vertx.circuitbreaker.CircuitBreakerOptions; import io.vertx.core.Future; import io.vertx.core.Vertx; -import io.vertx.core.VertxOptions; -import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpClient; -import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.json.JsonObject; -import io.vertx.core.net.SocketAddress; import io.vertx.ext.web.Router; import io.vertx.ext.web.client.WebClient; -import io.vertx.httpproxy.Body; import io.vertx.httpproxy.HttpProxy; import io.vertx.httpproxy.ProxyContext; import io.vertx.httpproxy.ProxyInterceptor; @@ -52,15 +41,12 @@ import lombok.extern.slf4j.Slf4j; @Order(value = 10) @Component public class DynamicBuildServer implements ApplicationRunner { + // TODO 后面可以和app挂钩 @Value("${server.vertx.server.default.port}") private Integer serverDefaultPort; - public static CircuitBreaker appCircuitBreaker; @Autowired private RedisTemplate redisTemplate; - @Autowired - private AppConfigService appConfigService; - @Autowired private RedisKeyConfig redisKeyConfig; @@ -68,65 +54,30 @@ public class DynamicBuildServer implements ApplicationRunner { public void run(ApplicationArguments args) throws Exception { // 初始化redis key redisKeyConfig.init(); - // 从redis同步app配置 - appConfigService.initAllAppConfig(); // 从redis同步vertx配置 - appConfigService.initVertxConfig(); + AppConfigHandle.initVertxConfig(redisTemplate); // 加载vertx、应用配置 appStartLoadData(); } + + /*** * 应用启动, 从redis读取配置,初始化vertx服务 + * @throws Exception */ - private void appStartLoadData() { - VertxConfig vertxConfig = AppConfigServiceImpl.getVertxConfig(); - // TODO 编解码线程池,后面优化协程等方式 - VertxOptions vertxOptions = new VertxOptions(); - loadVertxOptions(vertxConfig, vertxOptions); - - Vertx VERTX = Vertx.vertx(vertxOptions); - -// CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", VERTX, -// new CircuitBreakerOptions().setMaxFailures(3).setMaxRetries(5).setTimeout(2000) -// .setFallbackOnFailure(true) -// ).openHandler(v -> { -// log.info("Circuit opened"); -// }).closeHandler(v -> { -// log.info("Circuit closed"); -// });//.retryPolicy(retryCount -> retryCount * 100L); - - appCircuitBreaker = CircuitBreaker.create("my-circuit-breaker", VERTX, new CircuitBreakerOptions().setMaxFailures(3) // 最大失败数 - .setTimeout(2000) // 超时时间 - .setFallbackOnFailure(true) // 失败后是否调用回退函数(fallback) - .setResetTimeout(10000) // 在开启状态下,尝试重试之前所需时间 - ).openHandler(v -> { - log.info("Circuit open"); - }).halfOpenHandler(v -> { - log.info("Circuit halfOpen"); - }).closeHandler(v -> { - log.info("Circuit close"); - }); + private void appStartLoadData() throws Exception { + Vertx vertx = AppConfigHandle.createVertx(); + // 从redis同步app配置 + AppConfigHandle.initAllAppConfig(redisTemplate); - CircuitBreaker interfaceBreaker = CircuitBreaker.create("my-circuit-breaker", VERTX, new CircuitBreakerOptions().setMaxFailures(3) // 最大失败数 - .setTimeout(2000) // 超时时间 - .setFallbackOnFailure(true) // 失败后是否调用回退函数(fallback) - .setResetTimeout(10000) // 在开启状态下,尝试重试之前所需时间 - ).openHandler(v -> { - log.info("Circuit open"); - }).halfOpenHandler(v -> { - log.info("Circuit halfOpen"); - }).closeHandler(v -> { - log.info("Circuit close"); - }); - + VertxConfig vertxConfig = AppConfigHandle.getVertxConfig(); // 创建HTTP监听 // 所有ip都能访问 HttpServerOptions httpServerOptions = new HttpServerOptions().setHost("0.0.0.0"); - HttpServer server = VERTX.createHttpServer(httpServerOptions); - Router mainHttpRouter = Router.router(VERTX); - Integer serverPort = (vertxConfig == null || vertxConfig.getPort() == null) ? serverDefaultPort - : vertxConfig.getPort(); + HttpServer server = vertx.createHttpServer(httpServerOptions); + Router mainHttpRouter = Router.router(vertx); + Integer serverPort = vertxConfig.getPort() == null ? serverDefaultPort : vertxConfig.getPort(); log.info("serverPort:{}", serverPort); server.requestHandler(mainHttpRouter).listen(serverPort, h -> { if (h.succeeded()) { @@ -142,72 +93,36 @@ public class DynamicBuildServer implements ApplicationRunner { // clientOptions.setHttp2KeepAliveTimeout(1); // clientOptions.setIdleTimeout(1000); // 连接空闲超时 毫秒 // HttpClient proxyClient = VERTX.createHttpClient(clientOptions); - HttpClient proxyClient = VERTX.createHttpClient(); + HttpClient proxyClient = vertx.createHttpClient(); HttpProxy proxy = HttpProxy.reverseProxy(proxyClient); proxy.originSelector(request -> Future.succeededFuture(ProxyTool.resolveOriginAddress(request))); proxy.addInterceptor(new ProxyInterceptor() { @Override public Future handleProxyRequest(ProxyContext context) { - // 修改uri context.request().setURI(); - String sacAppHeaderKey = context.request().headers().get(AppConfigServiceImpl.getSacAppHeaderKey()); - log.info("addInterceptor uri appCode:{}", sacAppHeaderKey); +// if(StringUtils.equals(sacAppHeaderKey, "dsafdsfadafhappC")) { +// // 会跳转到 RestfulFailureHandlerImpl +// throw new HttpException(10003); +// } return context.sendRequest(); } @Override public Future handleProxyResponse(ProxyContext context) { // 调试代码,获取reponse body - Filter filter = new Filter(); - ProxyResponse proxyResponse = context.response(); - Body body = proxyResponse.getBody(); - proxyResponse.setBody(Body.body(filter.init(context.request().getURI(), body.stream(), false))); +// Filter filter = new Filter(); +// ProxyResponse proxyResponse = context.response(); +// Body body = proxyResponse.getBody(); +// proxyResponse.setBody(Body.body(filter.init(context.request().getURI(), body.stream(), false))); // 继续拦截链 return context.sendResponse(); } }); - WebClient mainWebClient = WebClient.create(VERTX); - // mainHttpRouter.route().handler(ProxyHandler.create(proxy)); + WebClient mainWebClient = WebClient.create(vertx); - // TODO 实例化方式 从 VertxConfig 读取 - String rateLimitModel = vertxConfig.getRateLimitModel() != null - && StringUtils.equals(vertxConfig.getRateLimitModel(), "redis") ? "redis" : "local"; - mainHttpRouter.route().handler(RateLimitHandler.create(rateLimitModel)).handler(BodyHandler.create()) - .handler(ProxyHandler.create(mainWebClient, proxy)) - .failureHandler(RestfulFailureHandler.create()); - // mainHttpRouter.route().handler(BodyHandler.create()).handler(ProxyHandler.create(mainWebClient,proxy)); - - // 服务健康检测重试 -// Integer periodicTime = AppConfigServiceImpl.getVertxConfig().getAddressRetryStrategy() != null -// && AppConfigServiceImpl.getVertxConfig().getAddressRetryStrategy().getPeriodicTime() > 0 -// ? AppConfigServiceImpl.getVertxConfig().getAddressRetryStrategy().getPeriodicTime() -// : 3; -// -// // TODO 是否开启健康检测 -// long timerID = VERTX.setPeriodic(periodicTime, id -> { -// Set set = redisTemplate.opsForZSet().range(RedisKeyConfig.APP_CONFIG_SET_KEY, 0, -1); -// for (String appCode : set) { -// Set setAddressRetryStrategy = redisTemplate.opsForZSet() -// .range(RedisKeyConfig.VERTX_ADDRESS_RETRY_STRATEGY_SET_KEY + ":" + appCode, 0, -1); -// for (String address : setAddressRetryStrategy) { -// // 发起请求,测试服务是否可用 -// // TODO 调用后端配置的健康检测地址 -// } -// -// } -// }); + String rateLimitModel = vertxConfig.getRateLimitModel(); + mainHttpRouter.route().handler(ParameterCheckHandler.create()).handler(RateLimitHandler.create(rateLimitModel)).handler(BodyHandler.create()) + .handler(ProxyHandler.create(mainWebClient, proxy)).failureHandler(RestfulFailureHandler.create()); +// mainHttpRouter.route().handler(ProxyHandler.create(mainWebClient, proxy)); } - private void loadVertxOptions(VertxConfig vertxConfig, VertxOptions vertxOptions) { - long blockedThreadCheckInterval = vertxConfig == null || vertxConfig.getVertxOptionsConfig() == null ? -1 - : vertxConfig.getVertxOptionsConfig().getBlockedThreadCheckInterval(); - int workerPoolSize = vertxConfig == null || vertxConfig.getVertxOptionsConfig() == null ? -1 - : vertxConfig.getVertxOptionsConfig().getWorkerPoolSize(); - if (workerPoolSize != -1) { - vertxOptions.setWorkerPoolSize(workerPoolSize); - } - blockedThreadCheckInterval = 1000000L; - if (blockedThreadCheckInterval != -1) { - vertxOptions.setBlockedThreadCheckInterval(blockedThreadCheckInterval); // 不打印Thread blocked 阻塞日志 - } - } } diff --git a/sf-vertx/src/main/java/com/sf/vertx/service/AppConfigService.java b/sf-vertx/src/main/java/com/sf/vertx/service/AppConfigService.java index f805c5c..d110daa 100644 --- a/sf-vertx/src/main/java/com/sf/vertx/service/AppConfigService.java +++ b/sf-vertx/src/main/java/com/sf/vertx/service/AppConfigService.java @@ -2,17 +2,12 @@ package com.sf.vertx.service; import com.sf.vertx.api.pojo.AppConfig; import com.sf.vertx.api.pojo.VertxConfig; -import com.sf.vertx.arithmetic.roundRobin.SacLoadBalancing; public interface AppConfigService { - void initAllAppConfig() throws Exception; - - void saveAppConfig(String appConfig); + void saveAppConfig(AppConfig appConfig); void deleteAppConfig(AppConfig appConfig); - void initVertxConfig(); - void saveVertxConfig(VertxConfig vertxConfig); } diff --git a/sf-vertx/src/main/java/com/sf/vertx/service/impl/AppConfigServiceImpl.java b/sf-vertx/src/main/java/com/sf/vertx/service/impl/AppConfigServiceImpl.java index 236a23c..7399fe8 100644 --- a/sf-vertx/src/main/java/com/sf/vertx/service/impl/AppConfigServiceImpl.java +++ b/sf-vertx/src/main/java/com/sf/vertx/service/impl/AppConfigServiceImpl.java @@ -1,33 +1,17 @@ package com.sf.vertx.service.impl; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; - -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; -import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; -import com.alibaba.fastjson2.TypeReference; import com.sf.vertx.api.pojo.AppConfig; -import com.sf.vertx.api.pojo.Node; -import com.sf.vertx.api.pojo.RouteContent; -import com.sf.vertx.api.pojo.SacService; -import com.sf.vertx.api.pojo.Strategy; import com.sf.vertx.api.pojo.VertxConfig; -import com.sf.vertx.arithmetic.roundRobin.SacLoadBalancing; import com.sf.vertx.constans.RedisKeyConfig; +import com.sf.vertx.handle.AppConfigHandle; import com.sf.vertx.service.AppConfigService; -import com.sf.vertx.utils.ProxyTool; -import com.sf.vertx.utils.SpringUtils; -import cn.hutool.core.collection.ConcurrentHashSet; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -37,172 +21,20 @@ public class AppConfigServiceImpl implements AppConfigService { private String vertxEnvironment; @Autowired private RedisTemplate redisTemplate; - private static VertxConfig VERTX_CONFIG = new VertxConfig(); - private static final ConcurrentHashMap CACHE_APP_CONFIG = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap CACHE_APP_CURRENT_LIMITING_CONFIG = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap CACHE_API_CURRENT_LIMITING_CONFIG = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap CACHE_APP_SERVICE = new ConcurrentHashMap<>(); - private static ConcurrentHashMap SAC_LOADBALANCING_MAP = new ConcurrentHashMap(); - private static ConcurrentHashSet CACHE_DISABLED_APP = new ConcurrentHashSet(); - - @SuppressWarnings("unchecked") - public static void addAddressRetryStrategy(String address, String appCode) { - String setKey = RedisKeyConfig.VERTX_ADDRESS_RETRY_STRATEGY_SET_KEY + ":" + appCode; - String key = RedisKeyConfig.VERTX_ADDRESS_RETRY_STRATEGY_KEY + ":" + appCode + ":" + address; - RedisTemplate redisTemplate = SpringUtils.getBean("redisTemplate", RedisTemplate.class); - Long thresholdCount = redisTemplate.opsForValue().increment(key); - // log.info("redis app threshold: {}", redisTemplate.opsForValue().get(key)); - Integer timeWindow = VERTX_CONFIG.getAddressRetryStrategy() != null - && VERTX_CONFIG.getAddressRetryStrategy().getTimeWindow() > 0 - ? VERTX_CONFIG.getAddressRetryStrategy().getTimeWindow() - : 20; - if (thresholdCount == 1) { // 创建,才设置时间窗口 - redisTemplate.expire(key, timeWindow, TimeUnit.SECONDS); - } - Integer threshold = AppConfigServiceImpl.getVertxConfig().getAddressRetryStrategy() != null - && AppConfigServiceImpl.getVertxConfig().getAddressRetryStrategy().getThreshold() > 0 - ? AppConfigServiceImpl.getVertxConfig().getAddressRetryStrategy().getThreshold() - : 3; - if (thresholdCount > threshold) { - // 设置服务不可用 - redisTemplate.opsForZSet().add(setKey, appCode + ":" + address, 0); - } - } - - public static boolean appDataSecurity(String appCode) { - return CACHE_APP_CONFIG.get(appCode) != null && CACHE_APP_CONFIG.get(appCode).getDataSecurity() != null ? true - : false; - } - - public static Strategy getAppCurrentLimitingConfig(String appCode) { - return CACHE_APP_CURRENT_LIMITING_CONFIG.get(appCode); - } - - public static Strategy getApiCurrentLimitingConfig(String appCode) { - return CACHE_API_CURRENT_LIMITING_CONFIG.get(appCode); - } - - public static AppConfig getAppConfig(String appCode) { - return CACHE_APP_CONFIG.get(appCode); - } - - public static VertxConfig getVertxConfig() { - return VERTX_CONFIG; - } - - public static String getSacAppHeaderKey() { - return VERTX_CONFIG.getAppHeaderKey() != null ? VERTX_CONFIG.getAppHeaderKey() : "sacAppCode"; - } - - public static String getAppHeaderServiceName() { - return VERTX_CONFIG.getAppHeaderServiceName() != null ? VERTX_CONFIG.getAppHeaderServiceName() - : "sacAppServiceName"; - } - - /*** - * 加载vertx配置 - */ - public void initVertxConfig() { - String vertxConfigKey = RedisKeyConfig.VERTX_CONFIG_STRING_KEY; - String vertxConfigValue = redisTemplate.opsForValue().get(vertxConfigKey); - if (StringUtils.isNotBlank(vertxConfigValue)) { - VERTX_CONFIG = JSONObject.parseObject(vertxConfigValue, VertxConfig.class); - } - } - - private void initAppConfig(String appCode) { - String appCodeKey = RedisKeyConfig.APP_CONFIG_PREFIX_KEY + ":" + appCode; - String appCodeValue = redisTemplate.opsForValue().get(appCodeKey); - if (StringUtils.isNotBlank(appCodeValue)) { - AppConfig appConfig = JSON.parseObject(appCodeValue, new TypeReference() { - }); - CACHE_APP_CONFIG.put(appCode, appConfig); - - // app、api默认限流 - if(appConfig.getApiCurrentLimitingConfig() != null) { - CACHE_API_CURRENT_LIMITING_CONFIG.put(appCode, appConfig.getApiCurrentLimitingConfig()); - } - - if(appConfig.getAppCurrentLimitingConfig() != null) { - CACHE_APP_CURRENT_LIMITING_CONFIG.put(appCode, appConfig.getAppCurrentLimitingConfig()); - } - - - // app router负载均衡 - for (SacService sacService : appConfig.getService()) { - CACHE_APP_SERVICE.put(appCode + ";" + sacService.getServiceName(), sacService); - List nodeList = new ArrayList<>(); - // 获取service模式 - if (StringUtils.equals(sacService.getServiceModel(), "NORMAL")) { - Node node = new Node(); - node.setIp(sacService.getServerAddress().getHost()); - node.setPort(sacService.getServerAddress().getPort()); - node.setWeight(0); - node.setProtocol(sacService.getServerAddress().getProtocol()); - nodeList.add(node); - } else if (StringUtils.equals(sacService.getServiceModel(), "ROUTE")) { - if (sacService.getRouteConfig() != null - && StringUtils.equals(sacService.getRouteConfig().getRouteType(), "WEIGHT_ROUTE")) { - for (RouteContent routeContent : sacService.getRouteConfig().getRouteContent()) { - Node node = new Node(); - node.setIp(routeContent.getServerAddress().getHost()); - node.setPort(routeContent.getServerAddress().getPort()); - node.setWeight(routeContent.getWeight() != null && routeContent.getWeight() > 0 - ? routeContent.getWeight() - : 0); - node.setProtocol(routeContent.getServerAddress().getProtocol()); - nodeList.add(node); - } - } - } - - if (nodeList.size() > 0) { - // 初始化负载均衡算法 - String key = appCode + ";" + sacService.getServiceName(); - SacLoadBalancing sacLoadBalancing = ProxyTool.roundRobin(nodeList); - SAC_LOADBALANCING_MAP.put(key, sacLoadBalancing); - } - - } - } - } - - /*** - * 从redis加载数据 - * - * @throws Exception - */ - public void initAllAppConfig() throws Exception { - Set set = redisTemplate.opsForZSet().range(RedisKeyConfig.APP_CONFIG_SET_KEY, 0, -1); - for (String appCode : set) { - initAppConfig(appCode); - } - } - - public static SacLoadBalancing getSacLoadBalancing(String appCode, String serviceName) { - return SAC_LOADBALANCING_MAP.get(appCode + ";" + serviceName); - } - - public static SacService getSacService(String appCode, String serviceName) { - return CACHE_APP_SERVICE.get(appCode + ";" + serviceName); - } /*** * 新增、修改 * * @param appConfig */ - public void saveAppConfig(String appConfigStr) { - AppConfig appConfig = JSON.parseObject(appConfigStr, AppConfig.class); + public void saveAppConfig(AppConfig appConfig) { redisTemplate.opsForZSet().add(RedisKeyConfig.APP_CONFIG_SET_KEY, appConfig.getAppCode(), 0); String appCodeKey = RedisKeyConfig.APP_CONFIG_PREFIX_KEY + ":" + appConfig.getAppCode(); - redisTemplate.opsForValue().set(appCodeKey, appConfigStr); - + redisTemplate.opsForValue().set(appCodeKey, JSONObject.toJSONString(appConfig)); + // 初始化AppConfig本地缓存 - initAppConfig(appConfig.getAppCode()); - - // 删除禁用app列表 - CACHE_DISABLED_APP.remove(appConfig.getAppCode()); + AppConfigHandle.initAppConfig(redisTemplate, appConfig.getAppCode()); + } /*** @@ -215,8 +47,8 @@ public class AppConfigServiceImpl implements AppConfigService { String appCodeKey = RedisKeyConfig.APP_CONFIG_PREFIX_KEY + ":" + appConfig.getAppCode(); redisTemplate.delete(appCodeKey); - // 不动本地缓存, 入口控制app被禁用, 项目启动会加载最新配置 - CACHE_DISABLED_APP.add(appConfig.getAppCode()); + // 禁用本地缓存 + AppConfigHandle.addDisabledAppcode(appConfig.getAppCode()); } /*** @@ -227,6 +59,7 @@ public class AppConfigServiceImpl implements AppConfigService { public void saveVertxConfig(VertxConfig vertxConfig) { String vertxConfigKey = RedisKeyConfig.VERTX_CONFIG_STRING_KEY; redisTemplate.opsForValue().set(vertxConfigKey, JSONObject.toJSONString(vertxConfig)); - initVertxConfig(); + AppConfigHandle.initVertxConfig(redisTemplate); } + } diff --git a/sf-vertx/src/main/java/com/sf/vertx/utils/ProxyTool.java b/sf-vertx/src/main/java/com/sf/vertx/utils/ProxyTool.java index 3a33570..bb46ca2 100644 --- a/sf-vertx/src/main/java/com/sf/vertx/utils/ProxyTool.java +++ b/sf-vertx/src/main/java/com/sf/vertx/utils/ProxyTool.java @@ -1,24 +1,15 @@ package com.sf.vertx.utils; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.regex.Pattern; -import org.apache.commons.lang3.StringUtils; - -import com.sf.vertx.api.pojo.AppConfig; import com.sf.vertx.api.pojo.Node; -import com.sf.vertx.api.pojo.RouteContent; -import com.sf.vertx.api.pojo.SacService; import com.sf.vertx.arithmetic.roundRobin.SacLoadBalancing; import com.sf.vertx.arithmetic.roundRobin.WeightedRoundRobin; -import com.sf.vertx.service.impl.AppConfigServiceImpl; +import com.sf.vertx.handle.AppConfigHandle; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.net.SocketAddress; -import io.vertx.ext.web.handler.HttpException; import lombok.extern.slf4j.Slf4j; /*** @@ -29,38 +20,12 @@ import lombok.extern.slf4j.Slf4j; */ @Slf4j public class ProxyTool { - public final static Map _ERROR = new HashMap<>(); - public static final String DEFAULT_ERROR_MSG = "{\n" + " \"msg\": \"服务请求失败,请稍后再试\",\n" + " \"code\": 501,\n" + " \"data\": \"服务请求失败,请稍后再试\"\n" + "}"; - static { - _ERROR.put(400, "Bad Request"); - _ERROR.put(401, "Unauthorized"); - _ERROR.put(403, "Forbidden"); - _ERROR.put(404, "Not Found"); - _ERROR.put(413, "Request Entity Too Large"); - _ERROR.put(415, "Unsupported Media Type"); - _ERROR.put(500, "Internal Server Error"); - _ERROR.put(502, "Bad Gateway"); - _ERROR.put(503, "Service Unavailable"); - _ERROR.put(504, "Gateway Timeout"); - _ERROR.put(504, "Gateway Timeout"); - - _ERROR.put(10000, "无法找到路由地址"); - _ERROR.put(10001, "加解密算法传递错误"); - _ERROR.put(10003, "参数传递错误"); - - _ERROR.put(10101, "接口限流错误"); - _ERROR.put(10102, "应用限流错误"); - - _ERROR.put(10103, "{\n" + " \"msg\": \"接口连接失败\",\n" + " \"code\": 10103,\n" + " \"data\": \"接口连接失败\"\n" + "}"); // 接口连接失败 - _ERROR.put(10104, "{\n" + " \"msg\": \"应用连接失败\",\n" + " \"code\": 10103,\n" + " \"data\": \"应用连接失败\"\n" + "}"); // 应用连接失败 - }; - + public static SocketAddress resolveOriginAddress(HttpServerRequest request) { - String appCode = request.getHeader(AppConfigServiceImpl.getSacAppHeaderKey()); - String appHeaderServiceName = request.getHeader(AppConfigServiceImpl.getAppHeaderServiceName()); - log.info("uri:{}, header appCode:{},appHeaderServiceName:{}", request.uri(), appCode, appHeaderServiceName); - - SacLoadBalancing sacLoadBalancing = AppConfigServiceImpl.getSacLoadBalancing(appCode, appHeaderServiceName); + String appCode = request.getHeader(AppConfigHandle.getAppCodeHeaderKey()); + String apiCode = request.getHeader(AppConfigHandle.getApiCodeHeaderKey()); + log.info("uri:{}, header appCode:{},apiCode:{}", request.uri(), appCode, apiCode); + SacLoadBalancing sacLoadBalancing = AppConfigHandle.getLoadBalancing(appCode + ":" + apiCode); // TODO 区分https、http Node node = sacLoadBalancing.selectNode(); SocketAddress socketAddress = SocketAddress.inetSocketAddress(node.getPort(), node.getIp()); diff --git a/sf-vertx/src/main/java/examples/CircuitBreakerExamples.java b/sf-vertx/src/main/java/examples/CircuitBreakerExamples.java new file mode 100644 index 0000000..87fe2e8 --- /dev/null +++ b/sf-vertx/src/main/java/examples/CircuitBreakerExamples.java @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2011-2016 The original author or authors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ + +package examples; + +import io.vertx.circuitbreaker.CircuitBreaker; +import io.vertx.circuitbreaker.CircuitBreakerOptions; +import io.vertx.circuitbreaker.HystrixMetricHandler; +import io.vertx.circuitbreaker.RetryPolicy; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.Router; + +/** + * @author Clement Escoffier + */ +public class CircuitBreakerExamples { + + public void example1(Vertx vertx) { + CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, + new CircuitBreakerOptions() + .setMaxFailures(5) // number of failure before opening the circuit + .setTimeout(2000) // consider a failure if the operation does not succeed in time + .setFallbackOnFailure(true) // do we call the fallback on failure + .setResetTimeout(10000) // time spent in open state before attempting to re-try + ); + + // --- + // Store the circuit breaker in a field and access it as follows + // --- + + breaker.execute(promise -> { + // some code executing with the breaker + // the code reports failures or success on the given promise. + // if this promise is marked as failed, the breaker increased the + // number of failures + }).onComplete(ar -> { + // Get the operation result. + }); + } + + public void example2(Vertx vertx) { + CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, + new CircuitBreakerOptions().setMaxFailures(5).setTimeout(2000) + ); + + // --- + // Store the circuit breaker in a field and access it as follows + // --- + + breaker.execute(promise -> { + vertx.createHttpClient().request(HttpMethod.GET, 8080, "localhost", "/") + .compose(req -> req + .send() + .compose(resp -> { + if (resp.statusCode() != 200) { + return Future.failedFuture("HTTP error"); + } else { + return resp.body().map(Buffer::toString); + } + })).onComplete(promise); + }).onComplete(ar -> { + // Do something with the result + }); + } + + public void example3(Vertx vertx) { + CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, + new CircuitBreakerOptions().setMaxFailures(5).setTimeout(2000) + ); + + // --- + // Store the circuit breaker in a field and access it as follows + // --- + + breaker.executeWithFallback( + promise -> { + vertx.createHttpClient().request(HttpMethod.GET, 8080, "localhost", "/") + .compose(req -> req + .send() + .compose(resp -> { + if (resp.statusCode() != 200) { + return Future.failedFuture("HTTP error"); + } else { + return resp.body().map(Buffer::toString); + } + })).onComplete(promise); + }, v -> { + // Executed when the circuit is opened + return "Hello"; + }) + .onComplete(ar -> { + // Do something with the result + }); + } + + public void example4(Vertx vertx) { + CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, + new CircuitBreakerOptions().setMaxFailures(5).setTimeout(2000) + ).fallback(v -> { + // Executed when the circuit is opened. + return "hello"; + }); + + breaker.execute( + promise -> { + vertx.createHttpClient().request(HttpMethod.GET, 8080, "localhost", "/") + .compose(req -> req + .send() + .compose(resp -> { + if (resp.statusCode() != 200) { + return Future.failedFuture("HTTP error"); + } else { + return resp.body().map(Buffer::toString); + } + })).onComplete(promise); + }); + } + + public void example5(Vertx vertx) { + CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, + new CircuitBreakerOptions().setMaxFailures(5).setTimeout(2000) + ).openHandler(v -> { + System.out.println("Circuit opened"); + }).closeHandler(v -> { + System.out.println("Circuit closed"); + }); + + breaker.execute( + promise -> { + vertx.createHttpClient().request(HttpMethod.GET, 8080, "localhost", "/") + .compose(req -> req + .send() + .compose(resp -> { + if (resp.statusCode() != 200) { + return Future.failedFuture("HTTP error"); + } else { + return resp.body().map(Buffer::toString); + } + })).onComplete(promise); + }); + } + + public void example6(Vertx vertx) { + CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, + new CircuitBreakerOptions().setMaxFailures(5).setTimeout(2000) + ); + + Promise userPromise = Promise.promise(); + userPromise.future().onComplete(ar -> { + // Do something with the result + }); + + breaker.executeAndReportWithFallback( + userPromise, + promise -> { + vertx.createHttpClient().request(HttpMethod.GET, 8080, "localhost", "/") + .compose(req -> req + .send() + .compose(resp -> { + if (resp.statusCode() != 200) { + return Future.failedFuture("HTTP error"); + } else { + return resp.body().map(Buffer::toString); + } + })).onComplete(promise); + }, v -> { + // Executed when the circuit is opened + return "Hello"; + }); + } + + public void example7(Vertx vertx) { + // Enable notifications + CircuitBreakerOptions options = new CircuitBreakerOptions() + .setNotificationAddress(CircuitBreakerOptions.DEFAULT_NOTIFICATION_ADDRESS); + CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, new CircuitBreakerOptions(options)); + CircuitBreaker breaker2 = CircuitBreaker.create("my-second-circuit-breaker", vertx, new CircuitBreakerOptions(options)); + + // Create a Vert.x Web router + Router router = Router.router(vertx); + // Register the metric handler + router.get("/hystrix-metrics").handler(HystrixMetricHandler.create(vertx)); + + // Create the HTTP server using the router to dispatch the requests + vertx.createHttpServer() + .requestHandler(router) + .listen(8080); + + } + + public void example8(Vertx vertx) { + CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, + new CircuitBreakerOptions().setMaxFailures(5).setMaxRetries(5).setTimeout(2000) + ).openHandler(v -> { + System.out.println("Circuit opened"); + }).closeHandler(v -> { + System.out.println("Circuit closed"); + }).retryPolicy(RetryPolicy.exponentialDelayWithJitter(50, 500)); + + breaker.execute( + promise -> { + vertx.createHttpClient().request(HttpMethod.GET, 8080, "localhost", "/") + .compose(req -> req + .send() + .compose(resp -> { + if (resp.statusCode() != 200) { + return Future.failedFuture("HTTP error"); + } else { + return resp.body().map(Buffer::toString); + } + })).onComplete(promise); + }); + } + + public void example9(Vertx vertx) { + CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx); + breaker.failurePolicy(ar -> { + // A failure will be either a failed operation or a response with a status code other than 200 + if (ar.failed()) { + return true; + } + HttpClientResponse resp = ar.result(); + return resp.statusCode() != 200; + }); + + Future future = breaker.execute(promise -> { + vertx.createHttpClient() + .request(HttpMethod.GET, 8080, "localhost", "/") + .compose(request -> request.send() + // Complete when the body is fully received + .compose(response -> response.body().map(response))) + .onComplete(promise); + }); + } + + public void enableNotifications(CircuitBreakerOptions options) { + options.setNotificationAddress(CircuitBreakerOptions.DEFAULT_NOTIFICATION_ADDRESS); + } +} diff --git a/sf-vertx/src/main/java/examples/hystrix/HystrixExamples.java b/sf-vertx/src/main/java/examples/hystrix/HystrixExamples.java new file mode 100644 index 0000000..16e646f --- /dev/null +++ b/sf-vertx/src/main/java/examples/hystrix/HystrixExamples.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2011-2016 The original author or authors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ + +package examples.hystrix; + +import com.netflix.hystrix.HystrixCommand; +import io.vertx.core.Context; +import io.vertx.core.Vertx; + +/** + * Examples for Hystrix + * + * @author Clement Escoffier + */ +public class HystrixExamples { + + public void exampleHystrix1() { + HystrixCommand someCommand = getSomeCommandInstance(); + String result = someCommand.execute(); + } + + public void exampleHystrix2(Vertx vertx) { + HystrixCommand someCommand = getSomeCommandInstance(); + vertx.executeBlocking( + future -> future.complete(someCommand.execute())).onComplete(ar -> { + // back on the event loop + String result = ar.result(); + } + ); + } + + public void exampleHystrix3(Vertx vertx) { + vertx.runOnContext(v -> { + Context context = vertx.getOrCreateContext(); + HystrixCommand command = getSomeCommandInstance(); + command.observe().subscribe(result -> { + context.runOnContext(v2 -> { + // Back on context (event loop or worker) + String r = result; + }); + }); + }); + } + + private HystrixCommand getSomeCommandInstance() { + return null; + } +} diff --git a/sf-vertx/src/main/java/examples/hystrix/package-info.java b/sf-vertx/src/main/java/examples/hystrix/package-info.java new file mode 100644 index 0000000..21ac6ab --- /dev/null +++ b/sf-vertx/src/main/java/examples/hystrix/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2011-2016 The original author or authors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ + +@Source(translate = false) +package examples.hystrix; + +import io.vertx.docgen.Source; \ No newline at end of file diff --git a/sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreaker.java b/sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreaker.java new file mode 100644 index 0000000..273ae2e --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreaker.java @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2011-2016 The original author or authors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ + +package io.vertx.circuitbreaker; + +import io.vertx.circuitbreaker.impl.CircuitBreakerImpl; +import io.vertx.codegen.annotations.CacheReturn; +import io.vertx.codegen.annotations.Fluent; +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.*; + +import java.util.function.Function; + +/** + * An implementation of the circuit breaker pattern for Vert.x + * + * @author Clement Escoffier + */ +@VertxGen +public interface CircuitBreaker { + + /** + * Creates a new instance of {@link CircuitBreaker}. + * + * @param name the name + * @param vertx the Vert.x instance + * @param options the configuration option + * @return the created instance + */ + static CircuitBreaker create(String name, Vertx vertx, CircuitBreakerOptions options) { + return new CircuitBreakerImpl(name, vertx, options == null ? new CircuitBreakerOptions() : options); + } + + /** + * Creates a new instance of {@link CircuitBreaker}, with default options. + * + * @param name the name + * @param vertx the Vert.x instance + * @return the created instance + */ + static CircuitBreaker create(String name, Vertx vertx) { + return new CircuitBreakerImpl(name, vertx, new CircuitBreakerOptions()); + } + + /** + * Closes the circuit breaker. It stops sending events on its state on the event bus. + * This method is not related to the {@code close} state of the circuit breaker. To set the circuit breaker in the + * {@code close} state, use {@link #reset()}. + */ + @Fluent + CircuitBreaker close(); + + /** + * Sets a {@link Handler} invoked when the circuit breaker state switches to open. + * + * @param handler the handler, must not be {@code null} + * @return the current {@link CircuitBreaker} + */ + @Fluent + CircuitBreaker openHandler(Handler handler); + + /** + * Sets a {@link Handler} invoked when the circuit breaker state switches to half-open. + * + * @param handler the handler, must not be {@code null} + * @return the current {@link CircuitBreaker} + */ + @Fluent + CircuitBreaker halfOpenHandler(Handler handler); + + /** + * Sets a {@link Handler} invoked when the circuit breaker state switches to close. + * + * @param handler the handler, must not be {@code null} + * @return the current {@link CircuitBreaker} + */ + @Fluent + CircuitBreaker closeHandler(Handler handler); + + /** + * Executes the given operation with the circuit breaker control. The operation is generally calling an + * external system. The operation receives a {@link Promise} object as parameter and must + * call {@link Promise#complete(Object)} when the operation has terminated successfully. The operation must also + * call {@link Promise#fail(Throwable)} in case of failure. + *

+ * The operation is not invoked if the circuit breaker is open, and the given fallback is called immediately. The + * circuit breaker also monitor the completion of the operation before a configure timeout. The operation is + * considered as failed if it does not terminate in time. + *

+ * This method returns a {@link Future} object to retrieve the status and result of the operation, with the status + * being a success or a failure. If the fallback is called, the returned future is successfully completed with the + * value returned from the fallback. If the fallback throws an exception, the returned future is marked as failed. + * + * @param command the operation + * @param fallback the fallback function. It gets an exception as parameter and returns the fallback result + * @param the type of result + * @return a future object completed when the operation or its fallback completes + */ + Future executeWithFallback(Handler> command, Function fallback); + + /** + * Same as {@link #executeWithFallback(Handler, Function)} but using a callback. + * + * @param command the operation + * @param fallback the fallback + * @param handler the completion handler receiving either the operation result or the fallback result. The + * parameter is an {@link AsyncResult} because if the fallback is not called, the error is passed + * to the handler. + * @param the type of result + */ + default void executeWithFallback(Handler> command, Function fallback, + Handler> handler) { + Future fut = executeWithFallback(command, fallback); + fut.onComplete(handler); + } + + /** + * Same as {@link #executeWithFallback(Handler, Function)} but using the circuit breaker default fallback. + * + * @param command the operation + * @param the type of result + * @return a future object completed when the operation or its fallback completes + */ + Future execute(Handler> command); + + /** + * Same as {@link #executeWithFallback(Handler, Function)} but using the circuit breaker default fallback. + * + * @param command the operation + * @param handler the completion handler receiving either the operation result or the fallback result. The + * parameter is an {@link AsyncResult} because if the fallback is not called, the error is passed + * to the handler. + * @param the type of result + */ + default void execute(Handler> command, Handler> handler) { + Future fut = execute(command); + fut.onComplete(handler); + } + + /** + * Same as {@link #executeAndReportWithFallback(Promise, Handler, Function)} but using the circuit breaker default + * fallback. + * + * @param resultPromise the promise on which the operation result is reported + * @param command the operation + * @param the type of result + * @return the current {@link CircuitBreaker} + */ + @Fluent + CircuitBreaker executeAndReport(Promise resultPromise, Handler> command); + + /** + * Executes the given operation with the circuit breaker control. The operation is generally calling an + * external system. The operation receives a {@link Promise} object as parameter and must + * call {@link Promise#complete(Object)} when the operation has terminated successfully. The operation must also + * call {@link Promise#fail(Throwable)} in case of failure. + *

+ * The operation is not invoked if the circuit breaker is open, and the given fallback is called immediately. The + * circuit breaker also monitor the completion of the operation before a configure timeout. The operation is + * considered as failed if it does not terminate in time. + *

+ * Unlike {@link #executeWithFallback(Handler, Function)}, this method does return a {@link Future} object, but + * let the caller pass a {@link Future} object on which the result is reported. If the fallback is called, the future + * is successfully completed with the value returned by the fallback function. If the fallback throws an exception, + * the future is marked as failed. + * + * @param resultPromise the promise on which the operation result is reported + * @param command the operation + * @param fallback the fallback function. It gets an exception as parameter and returns the fallback result + * @param the type of result + * @return the current {@link CircuitBreaker} + */ + @Fluent + CircuitBreaker executeAndReportWithFallback(Promise resultPromise, Handler> command, + Function fallback); + + /** + * Sets a default {@link Function} invoked when the bridge is open to handle the "request", or on failure + * if {@link CircuitBreakerOptions#isFallbackOnFailure()} is enabled. + *

+ * The function gets the exception as parameter and returns the fallback result. + * + * @param handler the handler + * @return the current {@link CircuitBreaker} + */ + @Fluent + CircuitBreaker fallback(Function handler); + + /** + * Configures the failure policy for this circuit-breaker. + * + * @return the current {@link CircuitBreaker} + * @see FailurePolicy + */ + @Fluent + default CircuitBreaker failurePolicy(FailurePolicy failurePolicy) { + return this; + } + + /** + * Resets the circuit breaker state (number of failure set to 0 and state set to closed). + * + * @return the current {@link CircuitBreaker} + */ + @Fluent + CircuitBreaker reset(); + + /** + * Explicitly opens the circuit. + * + * @return the current {@link CircuitBreaker} + */ + @Fluent + CircuitBreaker open(); + + /** + * @return the current state. + */ + CircuitBreakerState state(); + + /** + * @return the current number of failures. + */ + long failureCount(); + + /** + * @return the name of the circuit breaker. + */ + @CacheReturn + String name(); + + /** + * @deprecated use {@link #retryPolicy(RetryPolicy)} instead + */ + @Fluent + @Deprecated + CircuitBreaker retryPolicy(Function retryPolicy); + + /** + * Set a {@link RetryPolicy} which computes a delay before retry execution. + */ + @Fluent + CircuitBreaker retryPolicy(RetryPolicy retryPolicy); +} diff --git a/sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreakerOptions.java b/sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreakerOptions.java new file mode 100644 index 0000000..8942771 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreakerOptions.java @@ -0,0 +1,392 @@ +/* + * Copyright (c) 2011-2016 The original author or authors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ + +package io.vertx.circuitbreaker; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; + +/** + * Circuit breaker configuration options. All time are given in milliseconds. + * + * @author Clement Escoffier + */ +@DataObject +@JsonGen(publicConverter = false) +public class CircuitBreakerOptions { + + /** + * Default timeout in milliseconds. + */ + public static final long DEFAULT_TIMEOUT = 10000L; + + /** + * Default number of failures. + */ + public static final int DEFAULT_MAX_FAILURES = 5; + + /** + * Default value of the fallback on failure property. + */ + public static final boolean DEFAULT_FALLBACK_ON_FAILURE = false; + + /** + * Default time before it attempts to re-close the circuit (half-open state) in milliseconds. + */ + public static final long DEFAULT_RESET_TIMEOUT = 30000; + + /** + * Whether circuit breaker state should be delivered only to local consumers by default = {@code true}. + */ + public static final boolean DEFAULT_NOTIFICATION_LOCAL_ONLY = true; + + /** + * A default address on which the circuit breakers can send their updates. + */ + public static final String DEFAULT_NOTIFICATION_ADDRESS = "vertx.circuit-breaker"; + + /** + * Default notification period in milliseconds. + */ + public static final long DEFAULT_NOTIFICATION_PERIOD = 2000; + + /** + * Default rolling window for metrics in milliseconds. + */ + public static final long DEFAULT_METRICS_ROLLING_WINDOW = 10000; + + /** + * Default number of buckets used for the rolling window. + */ + public static final int DEFAULT_METRICS_ROLLING_BUCKETS = 10; + + /** + * Default number of retries. + */ + private static final int DEFAULT_MAX_RETRIES = 0; + + /** + * The default rolling window span in milliseconds. + */ + private static final int DEFAULT_FAILURES_ROLLING_WINDOW = 10000; + + /** + * The operation timeout. + */ + private long timeout = DEFAULT_TIMEOUT; + + /** + * The max failures. + */ + private int maxFailures = DEFAULT_MAX_FAILURES; + + /** + * Whether or not the fallback should be called upon failures. + */ + private boolean fallbackOnFailure = DEFAULT_FALLBACK_ON_FAILURE; + + /** + * The reset timeout. + */ + private long resetTimeout = DEFAULT_RESET_TIMEOUT; + + /** + * Whether circuit breaker state should be delivered only to local consumers. + */ + private boolean notificationLocalOnly = DEFAULT_NOTIFICATION_LOCAL_ONLY; + + /** + * The event bus address on which the circuit breaker state is published. + */ + private String notificationAddress = null; + + /** + * The state publication period in ms. + */ + private long notificationPeriod = DEFAULT_NOTIFICATION_PERIOD; + + /** + * The number of retries + */ + private int maxRetries = DEFAULT_MAX_RETRIES; + + /** + * The metric rolling window in ms. + */ + private long metricsRollingWindow = DEFAULT_METRICS_ROLLING_WINDOW; + + /** + * The number of buckets used for the metric rolling window. + */ + private int metricsRollingBuckets = DEFAULT_METRICS_ROLLING_BUCKETS; + + /** + * The failure rolling window in ms. + */ + private long failuresRollingWindow = DEFAULT_FAILURES_ROLLING_WINDOW; + + /** + * Creates a new instance of {@link CircuitBreakerOptions} using the default values. + */ + public CircuitBreakerOptions() { + // Empty constructor + } + + /** + * Creates a new instance of {@link CircuitBreakerOptions} by copying the other instance. + * + * @param other the instance fo copy + */ + public CircuitBreakerOptions(CircuitBreakerOptions other) { + this.timeout = other.timeout; + this.maxFailures = other.maxFailures; + this.fallbackOnFailure = other.fallbackOnFailure; + this.notificationLocalOnly = other.notificationLocalOnly; + this.notificationAddress = other.notificationAddress; + this.notificationPeriod = other.notificationPeriod; + this.resetTimeout = other.resetTimeout; + this.maxRetries = other.maxRetries; + this.metricsRollingBuckets = other.metricsRollingBuckets; + this.metricsRollingWindow = other.metricsRollingWindow; + this.failuresRollingWindow = other.failuresRollingWindow; + } + + /** + * Creates a new instance of {@link CircuitBreakerOptions} from the given json object. + * + * @param json the json object + */ + public CircuitBreakerOptions(JsonObject json) { + this(); + CircuitBreakerOptionsConverter.fromJson(json, this); + } + + /** + * @return a json object representing the current configuration. + */ + public JsonObject toJson() { + JsonObject json = new JsonObject(); + CircuitBreakerOptionsConverter.toJson(this, json); + return json; + } + + /** + * @return the maximum number of failures before opening the circuit. + */ + public int getMaxFailures() { + return maxFailures; + } + + /** + * Sets the maximum number of failures before opening the circuit. + * + * @param maxFailures the number of failures. + * @return the current {@link CircuitBreakerOptions} instance + */ + public CircuitBreakerOptions setMaxFailures(int maxFailures) { + this.maxFailures = maxFailures; + return this; + } + + /** + * @return the configured timeout in milliseconds. + */ + public long getTimeout() { + return timeout; + } + + /** + * Sets the timeout in milliseconds. If an action is not completed before this timeout, the action is considered as + * a failure. + * + * @param timeoutInMs the timeout, -1 to disable the timeout + * @return the current {@link CircuitBreakerOptions} instance + */ + public CircuitBreakerOptions setTimeout(long timeoutInMs) { + this.timeout = timeoutInMs; + return this; + } + + /** + * @return whether or not the fallback is executed on failures, even when the circuit is closed. + */ + public boolean isFallbackOnFailure() { + return fallbackOnFailure; + } + + /** + * Sets whether or not the fallback is executed on failure, even when the circuit is closed. + * + * @param fallbackOnFailure {@code true} to enable it. + * @return the current {@link CircuitBreakerOptions} instance + */ + public CircuitBreakerOptions setFallbackOnFailure(boolean fallbackOnFailure) { + this.fallbackOnFailure = fallbackOnFailure; + return this; + } + + /** + * @return the time in milliseconds before it attempts to re-close the circuit (by going to the half-open state). + */ + public long getResetTimeout() { + return resetTimeout; + } + + /** + * Sets the time in ms before it attempts to re-close the circuit (by going to the half-open state). If the circuit + * is closed when the timeout is reached, nothing happens. {@code -1} disables this feature. + * + * @param resetTimeout the time in ms + * @return the current {@link CircuitBreakerOptions} instance + */ + public CircuitBreakerOptions setResetTimeout(long resetTimeout) { + this.resetTimeout = resetTimeout; + return this; + } + + /** + * @return {@code true} if circuit breaker state should be delivered only to local consumers, otherwise {@code false} + */ + public boolean isNotificationLocalOnly() { + return notificationLocalOnly; + } + + /** + * Whether circuit breaker state should be delivered only to local consumers. + * + * @param notificationLocalOnly {@code true} if circuit breaker state should be delivered only to local consumers, otherwise {@code false} + * @return the current {@link CircuitBreakerOptions} instance + */ + public CircuitBreakerOptions setNotificationLocalOnly(boolean notificationLocalOnly) { + this.notificationLocalOnly = notificationLocalOnly; + return this; + } + + /** + * @return the eventbus address on which the circuit breaker events are published. {@code null} if this feature has + * been disabled. + */ + public String getNotificationAddress() { + return notificationAddress; + } + + /** + * Sets the event bus address on which the circuit breaker publish its state change. + * + * @param notificationAddress the address, {@code null} to disable this feature. + * @return the current {@link CircuitBreakerOptions} instance + */ + public CircuitBreakerOptions setNotificationAddress(String notificationAddress) { + this.notificationAddress = notificationAddress; + return this; + } + + /** + * @return the the period in milliseconds where the circuit breaker send a notification about its state. + */ + public long getNotificationPeriod() { + return notificationPeriod; + } + + /** + * Configures the period in milliseconds where the circuit breaker send a notification on the event bus with its + * current state. + * + * @param notificationPeriod the period, 0 to disable this feature. + * @return the current {@link CircuitBreakerOptions} instance + */ + public CircuitBreakerOptions setNotificationPeriod(long notificationPeriod) { + this.notificationPeriod = notificationPeriod; + return this; + } + + /** + * @return the configured rolling window for metrics. + */ + public long getMetricsRollingWindow() { + return metricsRollingWindow; + } + + /** + * Sets the rolling window used for metrics. + * + * @param metricsRollingWindow the period in milliseconds. + * @return the current {@link CircuitBreakerOptions} instance + */ + public CircuitBreakerOptions setMetricsRollingWindow(long metricsRollingWindow) { + this.metricsRollingWindow = metricsRollingWindow; + return this; + } + + /** + * @return the configured rolling window for failures. + */ + public long getFailuresRollingWindow() { + return failuresRollingWindow; + } + + /** + * Sets the rolling window used for metrics. + * + * @param metricsRollingWindow the period in milliseconds. + * @return the current {@link CircuitBreakerOptions} instance + */ + public CircuitBreakerOptions setFailuresRollingWindow(long failureRollingWindow) { + this.failuresRollingWindow = failureRollingWindow; + return this; + } + + /** + * @return the configured number of buckets the rolling window is divided into. + */ + public int getMetricsRollingBuckets() { + return metricsRollingBuckets; + } + + /** + * Sets the configured number of buckets the rolling window is divided into. + * + * The following must be true - metrics.rollingStats.timeInMilliseconds % metrics.rollingStats.numBuckets == 0 - otherwise it will throw an exception. + * + * In other words, 10000/10 is okay, so is 10000/20 but 10000/7 is not. + * + * @param metricsRollingBuckets the number of rolling buckets. + * @return the current {@link CircuitBreakerOptions} instance + */ + public CircuitBreakerOptions setMetricsRollingBuckets(int metricsRollingBuckets) { + this.metricsRollingBuckets = metricsRollingBuckets; + return this; + } + + /** + * @return the number of times the circuit breaker tries to redo the operation before failing + */ + public int getMaxRetries() { + return maxRetries; + } + + /** + * Configures the number of times the circuit breaker tries to redo the operation before failing. + * + * @param maxRetries the number of retries, 0 to disable this feature. + * @return the current {@link CircuitBreakerOptions} instance + */ + public CircuitBreakerOptions setMaxRetries(int maxRetries) { + this.maxRetries = maxRetries; + return this; + } +} diff --git a/sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreakerOptionsConverter.java b/sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreakerOptionsConverter.java new file mode 100644 index 0000000..b2152c0 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreakerOptionsConverter.java @@ -0,0 +1,101 @@ +package io.vertx.circuitbreaker; + +import io.vertx.core.json.JsonObject; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.impl.JsonUtil; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Base64; + +/** + * Converter and mapper for {@link io.vertx.circuitbreaker.CircuitBreakerOptions}. + * NOTE: This class has been automatically generated from the {@link io.vertx.circuitbreaker.CircuitBreakerOptions} original class using Vert.x codegen. + */ +public class CircuitBreakerOptionsConverter { + + + private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER; + private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER; + + static void fromJson(Iterable> json, CircuitBreakerOptions obj) { + for (java.util.Map.Entry member : json) { + switch (member.getKey()) { + case "failuresRollingWindow": + if (member.getValue() instanceof Number) { + obj.setFailuresRollingWindow(((Number)member.getValue()).longValue()); + } + break; + case "fallbackOnFailure": + if (member.getValue() instanceof Boolean) { + obj.setFallbackOnFailure((Boolean)member.getValue()); + } + break; + case "maxFailures": + if (member.getValue() instanceof Number) { + obj.setMaxFailures(((Number)member.getValue()).intValue()); + } + break; + case "maxRetries": + if (member.getValue() instanceof Number) { + obj.setMaxRetries(((Number)member.getValue()).intValue()); + } + break; + case "metricsRollingBuckets": + if (member.getValue() instanceof Number) { + obj.setMetricsRollingBuckets(((Number)member.getValue()).intValue()); + } + break; + case "metricsRollingWindow": + if (member.getValue() instanceof Number) { + obj.setMetricsRollingWindow(((Number)member.getValue()).longValue()); + } + break; + case "notificationAddress": + if (member.getValue() instanceof String) { + obj.setNotificationAddress((String)member.getValue()); + } + break; + case "notificationLocalOnly": + if (member.getValue() instanceof Boolean) { + obj.setNotificationLocalOnly((Boolean)member.getValue()); + } + break; + case "notificationPeriod": + if (member.getValue() instanceof Number) { + obj.setNotificationPeriod(((Number)member.getValue()).longValue()); + } + break; + case "resetTimeout": + if (member.getValue() instanceof Number) { + obj.setResetTimeout(((Number)member.getValue()).longValue()); + } + break; + case "timeout": + if (member.getValue() instanceof Number) { + obj.setTimeout(((Number)member.getValue()).longValue()); + } + break; + } + } + } + + static void toJson(CircuitBreakerOptions obj, JsonObject json) { + toJson(obj, json.getMap()); + } + + static void toJson(CircuitBreakerOptions obj, java.util.Map json) { + json.put("failuresRollingWindow", obj.getFailuresRollingWindow()); + json.put("fallbackOnFailure", obj.isFallbackOnFailure()); + json.put("maxFailures", obj.getMaxFailures()); + json.put("maxRetries", obj.getMaxRetries()); + json.put("metricsRollingBuckets", obj.getMetricsRollingBuckets()); + json.put("metricsRollingWindow", obj.getMetricsRollingWindow()); + if (obj.getNotificationAddress() != null) { + json.put("notificationAddress", obj.getNotificationAddress()); + } + json.put("notificationLocalOnly", obj.isNotificationLocalOnly()); + json.put("notificationPeriod", obj.getNotificationPeriod()); + json.put("resetTimeout", obj.getResetTimeout()); + json.put("timeout", obj.getTimeout()); + } +} diff --git a/sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreakerState.java b/sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreakerState.java new file mode 100644 index 0000000..e4de4df --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/circuitbreaker/CircuitBreakerState.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2011-2016 The original author or authors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ + +package io.vertx.circuitbreaker; + +import io.vertx.codegen.annotations.VertxGen; + +/** + * Circuit breaker states. + * + * @author Clement Escoffier + */ +@VertxGen +public enum CircuitBreakerState { + /** + * The {@code OPEN} state. The circuit breaker is executing the fallback, and switches to the {@link #HALF_OPEN} + * state after the specified time. + */ + OPEN, + /** + * The {@code CLOSED} state. The circuit breaker lets invocations pass and collects the failures. IF the number of + * failures reach the specified threshold, the cricuit breaker switches to the {@link #OPEN} state. + */ + CLOSED, + /** + * The {@code HALF_OPEN} state. The circuit breaker has been opened, and is now checking the current situation. It + * lets pass the next invocation and determines from the result (failure or success) if the circuit breaker can + * be switched to the {@link #CLOSED} state again. + */ + HALF_OPEN +} diff --git a/sf-vertx/src/main/java/io/vertx/circuitbreaker/FailurePolicy.java b/sf-vertx/src/main/java/io/vertx/circuitbreaker/FailurePolicy.java new file mode 100644 index 0000000..f719d97 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/circuitbreaker/FailurePolicy.java @@ -0,0 +1,35 @@ +package io.vertx.circuitbreaker; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; + +import java.util.function.Predicate; + +/** + * A failure policy for the {@link CircuitBreaker}. + *

+ * The default policy is to consider an asynchronous result as a failure if {@link AsyncResult#failed()} returns {@code true}. + * Nevertheless, sometimes this is not good enough. For example, an HTTP Client could return a response, but with an unexpected status code. + *

+ * In this case, a custom failure policy can be configured with {@link CircuitBreaker#failurePolicy(FailurePolicy)}. + */ +@VertxGen +public interface FailurePolicy extends Predicate> { + + /** + * The default policy, which considers an asynchronous result as a failure if {@link AsyncResult#failed()} returns {@code true}. + */ + static FailurePolicy defaultPolicy() { + return AsyncResult::failed; + } + + /** + * Invoked by the {@link CircuitBreaker} when an operation completes. + * + * @param future a completed future + * @return {@code true} if the asynchronous result should be considered as a failure, {@code false} otherwise + */ + @Override + boolean test(Future future); +} diff --git a/sf-vertx/src/main/java/io/vertx/circuitbreaker/HalfOpenCircuitException.java b/sf-vertx/src/main/java/io/vertx/circuitbreaker/HalfOpenCircuitException.java new file mode 100644 index 0000000..0f384ab --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/circuitbreaker/HalfOpenCircuitException.java @@ -0,0 +1,27 @@ +package io.vertx.circuitbreaker; + +/** + * Exception reported when the circuit breaker is open. + *

+ * For performance reason, this exception does not carry a stack trace. You are not allowed to set a stack trace or a + * cause to this exception. This immutability allows using a singleton instance. + * + * @author Clement Escoffier + */ +public class HalfOpenCircuitException extends RuntimeException { + + public HalfOpenCircuitException(String msg) { + super(msg, null, false, false); + } + + @Override + public void setStackTrace(StackTraceElement[] stackTrace) { + throw new UnsupportedOperationException(); + } + + @Override + public synchronized Throwable initCause(Throwable cause) { + throw new UnsupportedOperationException(); + } + +} diff --git a/sf-vertx/src/main/java/io/vertx/circuitbreaker/HystrixMetricHandler.java b/sf-vertx/src/main/java/io/vertx/circuitbreaker/HystrixMetricHandler.java new file mode 100644 index 0000000..89a3560 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/circuitbreaker/HystrixMetricHandler.java @@ -0,0 +1,50 @@ +package io.vertx.circuitbreaker; + +import io.vertx.circuitbreaker.impl.HystrixMetricEventStream; +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.ext.web.RoutingContext; + +/** + * A Vert.x web handler to expose the circuit breaker to the Hystrix dasbboard. The handler listens to the circuit + * breaker notifications sent on the event bus. + * + * @author Clement Escoffier + */ +@VertxGen +public interface HystrixMetricHandler extends Handler { + + /** + * Creates the handler, using the default notification address and listening to local messages only. + * + * @param vertx the Vert.x instance + * @return the handler + */ + static HystrixMetricHandler create(Vertx vertx) { + return create(vertx, CircuitBreakerOptions.DEFAULT_NOTIFICATION_ADDRESS); + } + + /** + * Creates the handler, listening only to local messages. + * + * @param vertx the Vert.x instance + * @param address the address to listen on the event bus + * @return the handler + */ + static HystrixMetricHandler create(Vertx vertx, String address) { + return create(vertx, address, CircuitBreakerOptions.DEFAULT_NOTIFICATION_LOCAL_ONLY); + } + + /** + * Creates the handler. + * + * @param vertx the Vert.x instance + * @param address the address to listen on the event bus + * @param localOnly whether the consumer should only receive messages sent from this Vert.x instance + * @return the handler + */ + static HystrixMetricHandler create(Vertx vertx, String address, boolean localOnly) { + return new HystrixMetricEventStream(vertx, address, localOnly); + } +} diff --git a/sf-vertx/src/main/java/io/vertx/circuitbreaker/OpenCircuitException.java b/sf-vertx/src/main/java/io/vertx/circuitbreaker/OpenCircuitException.java new file mode 100644 index 0000000..8df6717 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/circuitbreaker/OpenCircuitException.java @@ -0,0 +1,29 @@ +package io.vertx.circuitbreaker; + +/** + * Exception reported when the circuit breaker is open. + *

+ * For performance reason, this exception does not carry a stack trace. You are not allowed to set a stack trace or a + * cause to this exception. This immutability allows using a singleton instance. + * + * @author Clement Escoffier + */ +public class OpenCircuitException extends RuntimeException { + + public static OpenCircuitException INSTANCE = new OpenCircuitException(); + + private OpenCircuitException() { + super("open circuit", null, false, false); + } + + @Override + public void setStackTrace(StackTraceElement[] stackTrace) { + throw new UnsupportedOperationException(); + } + + @Override + public synchronized Throwable initCause(Throwable cause) { + throw new UnsupportedOperationException(); + } + +} diff --git a/sf-vertx/src/main/java/io/vertx/circuitbreaker/RetryPolicy.java b/sf-vertx/src/main/java/io/vertx/circuitbreaker/RetryPolicy.java new file mode 100644 index 0000000..4c87d8d --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/circuitbreaker/RetryPolicy.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2011-2022 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ + +package io.vertx.circuitbreaker; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.impl.Arguments; + +import java.util.concurrent.ThreadLocalRandom; + +import static java.lang.Math.*; + +/** + * A policy for retry execution. + */ +@VertxGen +@FunctionalInterface +public interface RetryPolicy { + + /** + * Create a constant delay retry policy. + * + * @param delay the constant delay in milliseconds + */ + static RetryPolicy constantDelay(long delay) { + Arguments.require(delay > 0, "delay must be strictly positive"); + return (failure, retryCount) -> delay; + } + + /** + * Create a linear delay retry policy. + * + * @param initialDelay the initial delay in milliseconds + * @param maxDelay maximum delay in milliseconds + */ + static RetryPolicy linearDelay(long initialDelay, long maxDelay) { + Arguments.require(initialDelay > 0, "initialDelay must be strictly positive"); + Arguments.require(maxDelay >= initialDelay, "maxDelay must be greater than initialDelay"); + return (failure, retryCount) -> min(maxDelay, initialDelay * retryCount); + } + + /** + * Create an exponential delay with jitter retry policy. + *

+ * Based on Full Jitter in Exponential Backoff And Jitter. + * + * @param initialDelay the initial delay in milliseconds + * @param maxDelay maximum delay in milliseconds + */ + static RetryPolicy exponentialDelayWithJitter(long initialDelay, long maxDelay) { + Arguments.require(initialDelay > 0, "initialDelay must be strictly positive"); + Arguments.require(maxDelay >= initialDelay, "maxDelay must be greater than initialDelay"); + return (failure, retryCount) -> { + ThreadLocalRandom random = ThreadLocalRandom.current(); + long delay = initialDelay * (1L << retryCount); + return random.nextLong(0, delay < 0 ? maxDelay : min(maxDelay, delay)); + }; + } + + /** + * Compute a delay in milliseconds before retry is executed. + * + * @param failure the failure passed to the operation {@link io.vertx.core.Promise} + * @param retryCount the number of times operation has been retried already + * @return a delay in milliseconds before retry is executed + */ + long delay(Throwable failure, int retryCount); + +} diff --git a/sf-vertx/src/main/java/io/vertx/circuitbreaker/TimeoutException.java b/sf-vertx/src/main/java/io/vertx/circuitbreaker/TimeoutException.java new file mode 100644 index 0000000..1ad8c2a --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/circuitbreaker/TimeoutException.java @@ -0,0 +1,29 @@ +package io.vertx.circuitbreaker; + +/** + * Exception reported when the monitored operation timed out. + *

+ * For performance reason, this exception does not carry a stack trace. You are not allowed to set a stack trace or a + * cause to this exception. This immutability allows using a singleton instance. + * + * @author Clement Escoffier + */ +public class TimeoutException extends RuntimeException { + + public static TimeoutException INSTANCE = new TimeoutException(); + + private TimeoutException() { + super("operation timeout", null, false, false); + } + + @Override + public void setStackTrace(StackTraceElement[] stackTrace) { + throw new UnsupportedOperationException(); + } + + @Override + public synchronized Throwable initCause(Throwable cause) { + throw new UnsupportedOperationException(); + } + +} diff --git a/sf-vertx/src/main/java/io/vertx/circuitbreaker/impl/CircuitBreakerImpl.java b/sf-vertx/src/main/java/io/vertx/circuitbreaker/impl/CircuitBreakerImpl.java new file mode 100644 index 0000000..9bd78bb --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/circuitbreaker/impl/CircuitBreakerImpl.java @@ -0,0 +1,563 @@ +/* + * Copyright (c) 2011-2016 The original author or authors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ + +package io.vertx.circuitbreaker.impl; + +import io.vertx.circuitbreaker.*; +import io.vertx.core.*; +import io.vertx.core.eventbus.DeliveryOptions; +import io.vertx.core.json.JsonObject; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +/** + * @author Clement Escoffier + */ +public class CircuitBreakerImpl implements CircuitBreaker { + + private static final Handler NOOP = (v) -> { + // Nothing... + }; + + private final Vertx vertx; + private final CircuitBreakerOptions options; + private final String name; + private final long periodicUpdateTask; + + private Handler openHandler = NOOP; + private Handler halfOpenHandler = NOOP; + private Handler closeHandler = NOOP; + private Function fallback = null; + private FailurePolicy failurePolicy = FailurePolicy.defaultPolicy(); + + private CircuitBreakerState state = CircuitBreakerState.CLOSED; + private RollingCounter rollingFailures; + + private final AtomicInteger passed = new AtomicInteger(); + + private final CircuitBreakerMetrics metrics; + private RetryPolicy retryPolicy = (failure, retryCount) -> 0L; + + public CircuitBreakerImpl(String name, Vertx vertx, CircuitBreakerOptions options) { + Objects.requireNonNull(name); + Objects.requireNonNull(vertx); + this.vertx = vertx; + this.name = name; + + if (options == null) { + this.options = new CircuitBreakerOptions(); + } else { + this.options = new CircuitBreakerOptions(options); + } + + this.rollingFailures = new RollingCounter(this.options.getFailuresRollingWindow() / 1000, TimeUnit.SECONDS); + + if (this.options.getNotificationAddress() != null) { + this.metrics = new CircuitBreakerMetrics(vertx, this, this.options); + sendUpdateOnEventBus(); + if (this.options.getNotificationPeriod() > 0) { + this.periodicUpdateTask = vertx.setPeriodic(this.options.getNotificationPeriod(), l -> sendUpdateOnEventBus()); + } else { + this.periodicUpdateTask = -1; + } + } else { + this.metrics = null; + this.periodicUpdateTask = -1; + } + } + + @Override + public CircuitBreaker close() { + if (metrics != null) { + if (periodicUpdateTask != -1) { + vertx.cancelTimer(periodicUpdateTask); + } + metrics.close(); + } + return this; + } + + @Override + public synchronized CircuitBreaker openHandler(Handler handler) { + Objects.requireNonNull(handler); + openHandler = handler; + return this; + } + + @Override + public synchronized CircuitBreaker halfOpenHandler(Handler handler) { + Objects.requireNonNull(handler); + halfOpenHandler = handler; + return this; + } + + @Override + public synchronized CircuitBreaker closeHandler(Handler handler) { + Objects.requireNonNull(handler); + closeHandler = handler; + return this; + } + + @Override + public CircuitBreaker fallback(Function handler) { + Objects.requireNonNull(handler); + fallback = handler; + return this; + } + + @Override + public CircuitBreaker failurePolicy(FailurePolicy failurePolicy) { + Objects.requireNonNull(failurePolicy); + this.failurePolicy = failurePolicy; + return this; + } + + /** + * A version of reset that can force the the state to `close` even if the circuit breaker is open. This is an + * internal API. + * + * @param force whether or not we force the state and allow an illegal transition + * @return the current circuit breaker. + */ + public synchronized CircuitBreaker reset(boolean force) { + rollingFailures.reset(); + + if (state == CircuitBreakerState.CLOSED) { + // Do nothing else. + return this; + } + + if (!force && state == CircuitBreakerState.OPEN) { + // Resetting the circuit breaker while we are in the open state is an illegal transition + return this; + } + + state = CircuitBreakerState.CLOSED; + closeHandler.handle(null); + sendUpdateOnEventBus(); + return this; + } + + @Override + public synchronized CircuitBreaker reset() { + return reset(false); + } + + private synchronized void sendUpdateOnEventBus() { + if (metrics != null) { + DeliveryOptions deliveryOptions = new DeliveryOptions() + .setLocalOnly(options.isNotificationLocalOnly()); + vertx.eventBus().publish(options.getNotificationAddress(), metrics.toJson(), deliveryOptions); + } + } + + @Override + public synchronized CircuitBreaker open() { + state = CircuitBreakerState.OPEN; + openHandler.handle(null); + sendUpdateOnEventBus(); + + // Set up the attempt reset timer + long period = options.getResetTimeout(); + if (period != -1) { + vertx.setTimer(period, l -> attemptReset()); + } + + return this; + } + + @Override + public synchronized long failureCount() { + return rollingFailures.count(); + } + + @Override + public synchronized CircuitBreakerState state() { + return state; + } + + private synchronized CircuitBreaker attemptReset() { + if (state == CircuitBreakerState.OPEN) { + passed.set(0); + state = CircuitBreakerState.HALF_OPEN; + halfOpenHandler.handle(null); + sendUpdateOnEventBus(); + } + return this; + } + + @Override + public CircuitBreaker executeAndReportWithFallback( + Promise userFuture, + Handler> command, + Function fallback) { + + Context context = vertx.getOrCreateContext(); + + CircuitBreakerState currentState; + synchronized (this) { + currentState = state; + } + + CircuitBreakerMetrics.Operation call = metrics != null ? metrics.enqueue() : null; + + // this future object tracks the completion of the operation + // This future is marked as failed on operation failures and timeout. + Promise operationResult = Promise.promise(); + + if (currentState == CircuitBreakerState.CLOSED) { + Future opFuture = operationResult.future(); + opFuture.onComplete(new ClosedCircuitCompletion<>(context, userFuture, fallback, call)); + if (options.getMaxRetries() > 0) { + executeOperation(context, command, retryFuture(context, 0, command, operationResult, call), call); + } else { + executeOperation(context, command, operationResult, call); + } + } else if (currentState == CircuitBreakerState.OPEN) { + // Fallback immediately + if (call != null) { + call.shortCircuited(); + } + invokeFallback(OpenCircuitException.INSTANCE, userFuture, fallback, call); + } else if (currentState == CircuitBreakerState.HALF_OPEN) { + if (passed.incrementAndGet() == 1) { + Future opFuture = operationResult.future(); + opFuture.onComplete(new HalfOpenedCircuitCompletion<>(context, userFuture, fallback, call)); + // Execute the operation + executeOperation(context, command, operationResult, call); + } else { + // Not selected, fallback. + if (call != null) { + call.shortCircuited(); + } + invokeFallback(OpenCircuitException.INSTANCE, userFuture, fallback, call); + } + } + return this; + } + + private Promise retryFuture(Context context, int retryCount, Handler> command, Promise + operationResult, CircuitBreakerMetrics.Operation call) { + Promise retry = Promise.promise(); + + retry.future().onComplete(event -> { + if (event.succeeded()) { + reset(); + context.runOnContext(v -> { + operationResult.complete(event.result()); + }); + return; + } + + CircuitBreakerState currentState; + synchronized (this) { + currentState = state; + } + + if (currentState == CircuitBreakerState.CLOSED) { + if (retryCount < options.getMaxRetries() - 1) { + executeRetryWithTimeout(event.cause(), retryCount, l -> { + context.runOnContext(v -> { + // Don't report timeout or error in the retry attempt, only the last one. + executeOperation(context, command, retryFuture(context, retryCount + 1, command, operationResult, null), + call); + }); + }); + } else { + executeRetryWithTimeout(event.cause(), retryCount, (l) -> { + context.runOnContext(v -> { + executeOperation(context, command, operationResult, call); + }); + }); + } + } else { + context.runOnContext(v -> operationResult.fail(OpenCircuitException.INSTANCE)); + } + }); + return retry; + } + + private void executeRetryWithTimeout(Throwable failure, int retryCount, Handler action) { + long retryTimeout = retryPolicy.delay(failure, retryCount + 1); + + if (retryTimeout > 0) { + vertx.setTimer(retryTimeout, (l) -> { + action.handle(null); + }); + } else { + action.handle(null); + } + } + + private void invokeFallback(Throwable reason, Promise userFuture, + Function fallback, CircuitBreakerMetrics.Operation operation) { + if (fallback == null) { + // No fallback, mark the user future as failed. + userFuture.fail(reason); + return; + } + + try { + T apply = fallback.apply(reason); + if (operation != null) { + operation.fallbackSucceed(); + } + userFuture.complete(apply); + } catch (Exception e) { + userFuture.fail(e); + if (operation != null) { + operation.fallbackFailed(); + } + } + } + + private void executeOperation(Context context, Handler> operation, Promise operationResult, + CircuitBreakerMetrics.Operation call) { + // We use an intermediate future to avoid the passed future to complete or fail after a timeout. + Promise passedFuture = Promise.promise(); + + // Execute the operation + if (options.getTimeout() != -1) { + long timerId = vertx.setTimer(options.getTimeout(), (l) -> { + context.runOnContext(v -> { + // Check if the operation has not already been completed + if (!operationResult.future().isComplete()) { + if (call != null) { + call.timeout(); + } + operationResult.fail(TimeoutException.INSTANCE); + } + // Else Operation has completed + }); + }); + passedFuture.future().onComplete(v -> vertx.cancelTimer(timerId)); + } + try { + passedFuture.future().onComplete(ar -> { + context.runOnContext(v -> { + if (ar.failed()) { + if (!operationResult.future().isComplete()) { + operationResult.fail(ar.cause()); + } + } else { + if (!operationResult.future().isComplete()) { + operationResult.complete(ar.result()); + } + } + }); + }); + + operation.handle(passedFuture); + } catch (Throwable e) { + context.runOnContext(v -> { + if (!operationResult.future().isComplete()) { + if (call != null) { + call.error(); + } + operationResult.fail(e); + } + }); + } + } + + @Override + public Future executeWithFallback(Handler> operation, Function fallback) { + Promise future = Promise.promise(); + executeAndReportWithFallback(future, operation, fallback); + return future.future(); + } + + public Future execute(Handler> operation) { + return executeWithFallback(operation, fallback); + } + + @Override + public CircuitBreaker executeAndReport(Promise resultFuture, Handler> operation) { + return executeAndReportWithFallback(resultFuture, operation, fallback); + } + + @Override + public String name() { + return name; + } + + private synchronized void incrementFailures() { + rollingFailures.increment(); + if (rollingFailures.count() >= options.getMaxFailures()) { + if (state != CircuitBreakerState.OPEN) { + open(); + } else { + // No need to do it in the previous case, open() do it. + // If open has been called, no need to send update, it will be done by the `open` method. + sendUpdateOnEventBus(); + } + } else { + // Number of failure has changed, send update. + sendUpdateOnEventBus(); + } + } + + /** + * For testing purpose only. + * + * @return retrieve the metrics. + */ + public JsonObject getMetrics() { + return metrics.toJson(); + } + + public CircuitBreakerOptions options() { + return options; + } + + @Override + public CircuitBreaker retryPolicy(Function retryPolicy) { + this.retryPolicy = (failure, retryCount) -> retryPolicy.apply(retryCount); + return this; + } + + @Override + public CircuitBreaker retryPolicy(RetryPolicy retryPolicy) { + this.retryPolicy = retryPolicy; + return this; + } + + public static class RollingCounter { + private Map window; + private long timeUnitsInWindow; + private TimeUnit windowTimeUnit; + + public RollingCounter(long timeUnitsInWindow, TimeUnit windowTimeUnit) { + this.windowTimeUnit = windowTimeUnit; + this.window = new LinkedHashMap<>((int) timeUnitsInWindow + 1); + this.timeUnitsInWindow = timeUnitsInWindow; + } + + public void increment() { + long timeSlot = windowTimeUnit.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS); + Long current = window.getOrDefault(timeSlot, 0L); + window.put(timeSlot, ++current); + + if (window.size() > timeUnitsInWindow) { + Iterator iterator = window.keySet().iterator(); + if (iterator.hasNext()) { + window.remove(iterator.next()); + } + } + } + + public long count() { + long windowStartTime = windowTimeUnit.convert(System.currentTimeMillis() - windowTimeUnit.toMillis(timeUnitsInWindow), TimeUnit.MILLISECONDS); + return window.entrySet().stream().filter(entry -> entry.getKey() >= windowStartTime).mapToLong(entry -> entry.getValue()).sum(); + } + + public void reset() { + window.clear(); + } + } + + @SuppressWarnings("unchecked") + private abstract class Completion implements Handler> { + + final Context context; + final Promise userFuture; + final Function fallback; + final CircuitBreakerMetrics.Operation call; + + protected Completion(Context context, Promise userFuture, Function fallback, CircuitBreakerMetrics.Operation call) { + this.context = context; + this.userFuture = userFuture; + this.fallback = fallback; + this.call = call; + } + + @Override + public void handle(AsyncResult ar) { + context.runOnContext(v -> { + if (failurePolicy.test(asFuture(ar))) { + failureAction(); + if (call != null) { + call.failed(); + } + if (options.isFallbackOnFailure()) { + Throwable throwable = ar.cause(); + if(ar.cause().getCause() == null) { + if(this instanceof CircuitBreakerImpl.HalfOpenedCircuitCompletion) { + throwable = new HalfOpenCircuitException(ar.cause().getMessage()); + } + } + //ar.cause(); + invokeFallback(throwable, userFuture, fallback, call); + } else { + userFuture.fail(ar.cause()); + } + } else { + if (call != null) { + call.complete(); + } + reset(); + //The event may pass due to a user given predicate. We still want to push up the failure for the user + //to do any work + userFuture.handle(ar); + } + }); + } + + private Future asFuture(AsyncResult ar) { + Future result; + if (ar instanceof Future) { + result = (Future) ar; + } else if (ar.succeeded()) { + result = Future.succeededFuture(ar.result()); + } else { + result = Future.failedFuture(ar.cause()); + } + return result; + } + + protected abstract void failureAction(); + } + + private class ClosedCircuitCompletion extends Completion { + + ClosedCircuitCompletion(Context context, Promise userFuture, Function fallback, CircuitBreakerMetrics.Operation call) { + super(context, userFuture, fallback, call); + } + + @Override + protected void failureAction() { + incrementFailures(); + } + } + + private class HalfOpenedCircuitCompletion extends Completion { + + HalfOpenedCircuitCompletion(Context context, Promise userFuture, Function fallback, CircuitBreakerMetrics.Operation call) { + super(context, userFuture, fallback, call); + } + + @Override + protected void failureAction() { + open(); + } + } +} diff --git a/sf-vertx/src/main/java/io/vertx/circuitbreaker/impl/CircuitBreakerMetrics.java b/sf-vertx/src/main/java/io/vertx/circuitbreaker/impl/CircuitBreakerMetrics.java new file mode 100644 index 0000000..691f9cf --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/circuitbreaker/impl/CircuitBreakerMetrics.java @@ -0,0 +1,354 @@ +package io.vertx.circuitbreaker.impl; + +import io.vertx.circuitbreaker.CircuitBreakerOptions; +import io.vertx.core.Vertx; +import io.vertx.core.impl.VertxInternal; +import io.vertx.core.json.JsonObject; +import org.HdrHistogram.Histogram; + +/** + * Circuit breaker metrics. + * + * @author Clement Escoffier + */ +public class CircuitBreakerMetrics { + private final CircuitBreakerImpl circuitBreaker; + private final String node; + + private final long circuitBreakerResetTimeout; + private final long circuitBreakerTimeout; + + // Global statistics + + private final RollingWindow rollingWindow; + + CircuitBreakerMetrics(Vertx vertx, CircuitBreakerImpl circuitBreaker, CircuitBreakerOptions options) { + this.circuitBreaker = circuitBreaker; + this.circuitBreakerTimeout = circuitBreaker.options().getTimeout(); + this.circuitBreakerResetTimeout = circuitBreaker.options().getResetTimeout(); + this.node = vertx.isClustered() ? ((VertxInternal) vertx).getClusterManager().getNodeId() : "local"; + this.rollingWindow = new RollingWindow(options.getMetricsRollingWindow(), options.getMetricsRollingBuckets()); + } + + private synchronized void evictOutdatedOperations() { + rollingWindow.updateTime(); + } + + public void close() { + // do nothing by default. + } + + class Operation { + final long begin; + private volatile long end; + private boolean complete; + private boolean failed; + private boolean timeout; + private boolean exception; + private boolean fallbackFailed; + private boolean fallbackSucceed; + private boolean shortCircuited; + + Operation() { + begin = System.nanoTime(); + } + + synchronized void complete() { + end = System.nanoTime(); + complete = true; + CircuitBreakerMetrics.this.complete(this); + } + + synchronized void failed() { + if (timeout || exception) { + // Already completed. + return; + } + end = System.nanoTime(); + failed = true; + CircuitBreakerMetrics.this.complete(this); + } + + synchronized void timeout() { + end = System.nanoTime(); + failed = false; + timeout = true; + CircuitBreakerMetrics.this.complete(this); + } + + synchronized void error() { + end = System.nanoTime(); + failed = false; + exception = true; + CircuitBreakerMetrics.this.complete(this); + } + + synchronized void fallbackFailed() { + fallbackFailed = true; + } + + synchronized void fallbackSucceed() { + fallbackSucceed = true; + } + + synchronized void shortCircuited() { + end = System.nanoTime(); + shortCircuited = true; + CircuitBreakerMetrics.this.complete(this); + } + + long durationInMs() { + return (end - begin) / 1_000_000; + } + } + + Operation enqueue() { + return new Operation(); + } + + public synchronized void complete(Operation operation) { + rollingWindow.add(operation); + } + + public synchronized JsonObject toJson() { + JsonObject json = new JsonObject(); + + // Configuration + json.put("resetTimeout", circuitBreakerResetTimeout); + json.put("timeout", circuitBreakerTimeout); + json.put("metricRollingWindow", rollingWindow.getMetricRollingWindowSizeInMs()); + json.put("name", circuitBreaker.name()); + json.put("node", node); + + // Current state + json.put("state", circuitBreaker.state()); + json.put("failures", circuitBreaker.failureCount()); + + // Global metrics + addSummary(json, rollingWindow.totalSummary(), MetricNames.TOTAL); + + // Window metrics + evictOutdatedOperations(); + addSummary(json, rollingWindow.windowSummary(), MetricNames.ROLLING); + + return json; + } + + private void addSummary(JsonObject json, RollingWindow.Summary summary, MetricNames names) { + long calls = summary.count(); + int errorCount = summary.failures + summary.exceptions + summary.timeouts; + + json.put(names.operationCountName, calls - summary.shortCircuited); + json.put(names.errorCountName, errorCount); + json.put(names.successCountName, summary.successes); + json.put(names.timeoutCountName, summary.timeouts); + json.put(names.exceptionCountName, summary.exceptions); + json.put(names.failureCountName, summary.failures); + + if (calls == 0) { + json.put(names.successPercentageName, 0); + json.put(names.errorPercentageName, 0); + } else { + json.put(names.successPercentageName, ((double) summary.successes / calls) * 100); + json.put(names.errorPercentageName, ((double) (errorCount) / calls) * 100); + } + + json.put(names.fallbackSuccessCountName, summary.fallbackSuccess); + json.put(names.fallbackFailureCountName, summary.fallbackFailure); + json.put(names.shortCircuitedCountName, summary.shortCircuited); + + addLatency(json, summary.statistics, names); + } + + + private void addLatency(JsonObject json, Histogram histogram, MetricNames names) { + json.put(names.latencyMeanName, histogram.getMean()); + json.put(names.latencyName, new JsonObject() + .put("0", histogram.getValueAtPercentile(0)) + .put("25", histogram.getValueAtPercentile(25)) + .put("50", histogram.getValueAtPercentile(50)) + .put("75", histogram.getValueAtPercentile(75)) + .put("90", histogram.getValueAtPercentile(90)) + .put("95", histogram.getValueAtPercentile(95)) + .put("99", histogram.getValueAtPercentile(99)) + .put("99.5", histogram.getValueAtPercentile(99.5)) + .put("100", histogram.getValueAtPercentile(100))); + } + + private enum MetricNames { + ROLLING("rolling"), TOTAL("total"); + + private final String operationCountName; + private final String errorCountName; + private final String successCountName; + private final String timeoutCountName; + private final String exceptionCountName; + private final String failureCountName; + private final String successPercentageName; + private final String errorPercentageName; + private final String fallbackSuccessCountName; + private final String fallbackFailureCountName; + private final String shortCircuitedCountName; + + private final String latencyMeanName; + private final String latencyName; + + MetricNames(String prefix){ + operationCountName = prefix + "OperationCount"; + errorCountName = prefix + "ErrorCount"; + successCountName = prefix + "SuccessCount"; + timeoutCountName = prefix + "TimeoutCount"; + exceptionCountName = prefix + "ExceptionCount"; + failureCountName = prefix + "FailureCount"; + successPercentageName = prefix + "SuccessPercentage"; + errorPercentageName = prefix + "ErrorPercentage"; + fallbackSuccessCountName = prefix + "FallbackSuccessCount"; + fallbackFailureCountName = prefix + "FallbackFailureCount"; + shortCircuitedCountName = prefix + "ShortCircuitedCount"; + + latencyName = prefix + "Latency"; + latencyMeanName = prefix + "LatencyMean"; + } + } + + private static class RollingWindow { + private final Summary history; + private final Summary[] buckets; + private final long bucketSizeInNs; + + RollingWindow(long windowSizeInMs, int numberOfBuckets) { + if (windowSizeInMs % numberOfBuckets != 0) { + throw new IllegalArgumentException("Window size should be divisible by number of buckets."); + } + this.buckets = new Summary[numberOfBuckets]; + for (int i = 0; i < buckets.length; i++) { + this.buckets[i] = new Summary(); + } + this.bucketSizeInNs = 1_000_000 * windowSizeInMs / numberOfBuckets; + this.history = new Summary(); + } + + public void add(Operation operation) { + getBucket(operation.end).add(operation); + } + + public Summary totalSummary() { + Summary total = new Summary(); + + total.add(history); + total.add(windowSummary()); + + return total; + } + + public Summary windowSummary() { + Summary window = new Summary(buckets[0].bucketIndex); + for (Summary bucket : buckets) { + window.add(bucket); + } + + return window; + } + + public void updateTime() { + getBucket(System.nanoTime()); + } + + private Summary getBucket(long timeInNs) { + long bucketIndex = timeInNs / bucketSizeInNs; + + //sample too old: + if (bucketIndex < buckets[0].bucketIndex) { + return history; + } + + shiftIfNecessary(bucketIndex); + + return buckets[(int) (bucketIndex - buckets[0].bucketIndex)]; + } + + private void shiftIfNecessary(long bucketIndex) { + long shiftUnlimited = bucketIndex - buckets[buckets.length - 1].bucketIndex; + if (shiftUnlimited <= 0) { + return; + } + int shift = (int) Long.min(buckets.length, shiftUnlimited); + + // Add old buckets to history + for(int i = 0; i < shift; i++) { + history.add(buckets[i]); + } + + System.arraycopy(buckets, shift, buckets, 0, buckets.length - shift); + + for(int i = buckets.length - shift; i < buckets.length; i++) { + buckets[i] = new Summary(bucketIndex + i + 1 - buckets.length); + } + } + + public long getMetricRollingWindowSizeInMs() { + return bucketSizeInNs * buckets.length / 1_000_000; + } + + private static class Summary { + final long bucketIndex; + final Histogram statistics; + + private int successes; + private int failures; + private int exceptions; + private int timeouts; + private int fallbackSuccess; + private int fallbackFailure; + private int shortCircuited; + + private Summary() { + this(-1); + } + + private Summary(long bucketIndex) { + this.bucketIndex = bucketIndex; + statistics = new Histogram(2); + } + + public void add(Summary other) { + statistics.add(other.statistics); + + successes += other.successes; + failures += other.failures; + exceptions += other.exceptions; + timeouts += other.timeouts; + fallbackSuccess += other.fallbackSuccess ; + fallbackFailure += other.fallbackFailure ; + shortCircuited += other.shortCircuited ; + } + + public void add(Operation operation) { + statistics.recordValue(operation.durationInMs()); + if (operation.complete) { + successes++; + } else if (operation.failed) { + failures++; + } else if (operation.exception) { + exceptions++; + } else if (operation.timeout) { + timeouts++; + } + + if (operation.fallbackSucceed) { + fallbackSuccess++; + } else if (operation.fallbackFailed) { + fallbackFailure++; + } + + if (operation.shortCircuited) { + shortCircuited++; + } + } + + public long count() { + return statistics.getTotalCount(); + } + } + } +} diff --git a/sf-vertx/src/main/java/io/vertx/circuitbreaker/impl/HystrixMetricEventStream.java b/sf-vertx/src/main/java/io/vertx/circuitbreaker/impl/HystrixMetricEventStream.java new file mode 100644 index 0000000..ccda096 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/circuitbreaker/impl/HystrixMetricEventStream.java @@ -0,0 +1,139 @@ +package io.vertx.circuitbreaker.impl; + +import io.vertx.circuitbreaker.CircuitBreakerState; +import io.vertx.circuitbreaker.HystrixMetricHandler; +import io.vertx.core.Vertx; +import io.vertx.core.eventbus.EventBus; +import io.vertx.core.eventbus.MessageConsumer; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + + +/** + * Implements a handler to serve the Vert.x circuit breaker metrics as a Hystrix circuit + * breaker. + * + * @author Clement Escoffier + */ +public class HystrixMetricEventStream implements HystrixMetricHandler { + + private final List connections = Collections.synchronizedList(new LinkedList<>()); + private AtomicInteger counter = new AtomicInteger(); + + public HystrixMetricEventStream(Vertx vertx, String address, boolean localOnly) { + Objects.requireNonNull(vertx); + Objects.requireNonNull(address); + + EventBus eventBus = vertx.eventBus(); + MessageConsumer consumer = localOnly ? eventBus.localConsumer(address) : eventBus.consumer(address); + consumer + .handler(message -> { + JsonObject json = build(message.body()); + int id = counter.incrementAndGet(); + String chunk = json.encode() + "\n\n"; + connections.forEach(resp -> { + try { + resp.write("id" + ": " + id + "\n"); + resp.write("data:" + chunk); + } catch (IllegalStateException e) { + // Connection close. + } + }); + }); + } + + private JsonObject build(JsonObject body) { + String state = body.getString("state"); + JsonObject json = new JsonObject(); + json.put("type", "HystrixCommand"); + json.put("name", body.getString("name")); + json.put("group", body.getString("node")); + json.put("currentTime", System.currentTimeMillis()); + json.put("isCircuitBreakerOpen", state.equalsIgnoreCase(CircuitBreakerState.OPEN.toString())); + json.put("errorPercentage", body.getInteger("rollingErrorPercentage", 0)); + json.put("errorCount", body.getInteger("rollingErrorCount", 0)); + json.put("requestCount", body.getInteger("rollingOperationCount", 0)); + json.put("rollingCountCollapsedRequests", 0); + json.put("rollingCountExceptionsThrown", body.getInteger("rollingExceptionCount", 0)); + json.put("rollingCountFailure", body.getInteger("rollingFailureCount", 0)); + json.put("rollingCountTimeout", body.getInteger("rollingTimeoutCount", 0)); + json.put("rollingCountFallbackFailure", body.getInteger("rollingFallbackFailureCount", 0)); + json.put("rollingCountFallbackRejection", body.getInteger("fallbackRejection", 0)); + json.put("rollingCountFallbackSuccess", body.getInteger("rollingFallbackSuccessCount", 0)); + json.put("rollingCountResponsesFromCache", 0); + json.put("rollingCountSemaphoreRejected", 0); + json.put("rollingCountShortCircuited", body.getInteger("rollingShortCircuitedCount", 0)); + json.put("rollingCountSuccess", body.getInteger("rollingSuccessCount", 0)); + json.put("rollingCountThreadPoolRejected", 0); + json.put("rollingCountTimeout", body.getInteger("rollingTimeoutCount", 0)); + json.put("rollingCountBadRequests", 0); + json.put("rollingCountEmit", 0); + json.put("rollingCountFallbackEmit", 0); + json.put("rollingCountFallbackMissing", 0); + json.put("rollingMaxConcurrentExecutionCount", 0); + json.put("currentConcurrentExecutionCount", 0); + json.put("latencyExecute_mean", body.getInteger("rollingLatencyMean", 0)); + json.put("latencyExecute", body.getJsonObject("rollingLatency", new JsonObject())); + json.put("latencyTotal_mean", body.getInteger("totalLatencyMean", 0)); + json.put("latencyTotal", body.getJsonObject("totalLatency", new JsonObject())); + + json.put("propertyValue_circuitBreakerRequestVolumeThreshold", 0); + json.put("propertyValue_circuitBreakerSleepWindowInMilliseconds", body.getLong("resetTimeout", 0L)); + json.put("propertyValue_circuitBreakerErrorThresholdPercentage", 0); + json.put("propertyValue_circuitBreakerForceOpen", false); + json.put("propertyValue_circuitBreakerForceClosed", false); + json.put("propertyValue_circuitBreakerEnabled", true); + json.put("propertyValue_executionIsolationStrategy", "THREAD"); + json.put("propertyValue_executionIsolationThreadTimeoutInMilliseconds", body.getLong("timeout", 0L)); + json.put("propertyValue_executionIsolationThreadInterruptOnTimeout", true); + json.put("propertyValue_executionIsolationThreadPoolKeyOverride", ""); + json.put("propertyValue_executionIsolationSemaphoreMaxConcurrentRequests", 0); + json.put("propertyValue_fallbackIsolationSemaphoreMaxConcurrentRequests", 0); + json.put("propertyValue_metricsRollingStatisticalWindowInMilliseconds", body.getLong("metricRollingWindow", 0L)); + json.put("propertyValue_requestCacheEnabled", false); + json.put("propertyValue_requestLogEnabled", false); + json.put("reportingHosts", 1); + return json; + } + + @Override + public void handle(RoutingContext rc) { + HttpServerResponse response = rc.response(); + response + .setChunked(true) + .putHeader(HttpHeaders.CONTENT_TYPE, "text/event-stream") + .putHeader(HttpHeaders.CACHE_CONTROL, "no-cache") + .putHeader(HttpHeaders.CONNECTION, HttpHeaders.KEEP_ALIVE); + + rc.request().connection() + .closeHandler(v -> { + connections.remove(response); + endQuietly(response); + }) + .exceptionHandler(t -> { + connections.remove(response); + rc.fail(t); + }); + + connections.add(response); + } + + private static void endQuietly(HttpServerResponse response) { + if (response.ended()) { + return; + } + try { + response.end(); + } catch (IllegalStateException e) { + // Ignore it. + } + } +} diff --git a/sf-vertx/src/main/java/io/vertx/circuitbreaker/package-info.java b/sf-vertx/src/main/java/io/vertx/circuitbreaker/package-info.java new file mode 100644 index 0000000..695c5db --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/circuitbreaker/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2011-2016 The original author or authors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * The Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php + * + * You may elect to redistribute this code under either of these licenses. + */ +@ModuleGen(name = "vertx-circuit-breaker", groupPackage = "io.vertx") +package io.vertx.circuitbreaker; + +import io.vertx.codegen.annotations.ModuleGen; diff --git a/sf-vertx/src/main/java/io/vertx/core/http/impl/SharedClientHttpStreamEndpoint.java b/sf-vertx/src/main/java/io/vertx/core/http/impl/SharedClientHttpStreamEndpoint.java index ae95802..8874d7e 100644 --- a/sf-vertx/src/main/java/io/vertx/core/http/impl/SharedClientHttpStreamEndpoint.java +++ b/sf-vertx/src/main/java/io/vertx/core/http/impl/SharedClientHttpStreamEndpoint.java @@ -14,9 +14,7 @@ import java.net.ConnectException; import java.util.List; import java.util.function.BiFunction; -import org.apache.commons.lang3.StringUtils; - -import com.sf.vertx.init.DynamicBuildServer; +import com.sf.vertx.handle.AppConfigHandle; import io.vertx.core.AsyncResult; import io.vertx.core.Future; @@ -85,7 +83,7 @@ class SharedClientHttpStreamEndpoint extends ClientHttpEndpointBase>> handler) { - DynamicBuildServer.appCircuitBreaker.executeWithFallback(promise -> { + AppConfigHandle.CONNECTION_CIRCUIT_BREAKER.executeWithFallback(promise -> { connector.httpConnect(context, ar -> { if (ar.succeeded()) { incRefCount(); diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java index a485dcc..7f856f4 100644 --- a/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java @@ -10,7 +10,6 @@ */ package io.vertx.httpproxy.impl; -import java.net.ConnectException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -21,11 +20,14 @@ import java.util.function.BiFunction; import org.apache.commons.lang3.StringUtils; import com.sf.vertx.api.pojo.DataSecurity; +import com.sf.vertx.constans.SacErrorCode; +import com.sf.vertx.handle.AppConfigHandle; import com.sf.vertx.security.MainSecurity; -import com.sf.vertx.service.impl.AppConfigServiceImpl; import com.sf.vertx.utils.ProxyTool; import io.vertx.circuitbreaker.CircuitBreaker; +import io.vertx.circuitbreaker.HalfOpenCircuitException; +import io.vertx.circuitbreaker.OpenCircuitException; import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.buffer.Buffer; @@ -36,6 +38,7 @@ import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.impl.NoStackTraceThrowable; import io.vertx.core.json.JsonObject; import io.vertx.core.net.NetSocket; import io.vertx.core.net.SocketAddress; @@ -175,20 +178,10 @@ public class ReverseProxy implements HttpProxy { * @param sc */ private void end(ProxyRequest proxyRequest, int sc) { - // TODO 处理反向代理返回结果 - if (ProxyTool._ERROR.containsKey(sc)) { - Buffer buffer = Buffer.buffer(ProxyTool._ERROR.get(sc)); - proxyRequest.response().release().setStatusCode(sc).putHeader("content-type", "application/json") - .putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(buffer.length())).setBody(Body.body(buffer)) - .send(); - } else { -// proxyRequest.response().release().setStatusCode(sc).putHeader(HttpHeaders.CONTENT_LENGTH, "0").setBody(null) -// .send(); - Buffer buffer = Buffer.buffer(ProxyTool.DEFAULT_ERROR_MSG); - proxyRequest.response().release().setStatusCode(sc).putHeader("content-type", "application/json") - .putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(ProxyTool.DEFAULT_ERROR_MSG.length())) - .setBody(Body.body(buffer)).send(); - } + JsonObject json = SacErrorCode.returnErrorMsg(sc); + proxyRequest.response().release().setStatusCode(500).putHeader("content-type", "application/json") + .putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(json.size())).setBody(Body.body(json.toBuffer())) + .send(); } private Future resolveOrigin(HttpServerRequest proxiedRequest) { @@ -247,36 +240,178 @@ public class ReverseProxy implements HttpProxy { } } - private Future sendProxyRequest(ProxyRequest proxyRequest) { - // TODO 服务熔断策略, 如果已经熔断,将剔除负载均衡策略 - // 发起一个请求 - String sacAppHeaderKey = proxyRequest.headers().get(AppConfigServiceImpl.getSacAppHeaderKey()); - if (StringUtils.isNotBlank(sacAppHeaderKey) && AppConfigServiceImpl.appDataSecurity(sacAppHeaderKey)) { - return Future.future(p -> { - SocketAddress socketAddress = ProxyTool.resolveOriginAddress(proxyRequest.proxiedRequest()); + private Future breakerAndSecurity(CircuitBreaker circuitBreaker, ProxyRequest proxyRequest) { + String appCode = proxyRequest.headers().get(AppConfigHandle.getAppCodeHeaderKey()); + SocketAddress socketAddress = ProxyTool.resolveOriginAddress(proxyRequest.proxiedRequest()); + return Future.future(p -> { + circuitBreaker.executeWithFallback(promise -> { mainWebClient.post(socketAddress.port(), socketAddress.host(), proxyRequest.getURI()) .putHeaders(proxyRequest.headers()) - .sendJson(bodyDecrypt(ctx.getBodyAsString(), sacAppHeaderKey), h -> { + .sendJson(bodyDecrypt(ctx.getBodyAsString(), appCode), h -> { if (h.succeeded()) { + log.info("==========uri:{},response http code:{}", proxyRequest.getURI(), + h.result().statusCode()); + if (h.result().statusCode() == 200) { + // promise.complete(); + promise.complete("1"); + // 释放资源 + proxyRequest.release(); + JsonObject responseData = h.result().bodyAsJsonObject(); + log.info("responseData:{}", responseData); + // 加密 + String dataStr = bodyEncrypt(responseData.toString(), appCode); + log.info("aesEncrypt dataStr:{}", dataStr); + Buffer buffer = Buffer.buffer(dataStr); + ProxyResponse proxyResponse = proxyRequest.response().setStatusCode(200) + .putHeader("content-type", "application/json") + .setBody(Body.body(buffer)); + p.complete(proxyResponse); + } else { + // Throwable throwable = new Throwable("error port"); + // promise.fail(throwable); + promise.fail("2"); + } + } else { + // end(proxyRequest, 502); + // Throwable throwable = new Throwable("error port"); + // promise.fail(throwable); + promise.fail("2"); + } + }); + }, v -> { + // 需要传递当前状态half-open , close, 还是统计失败次数 + log.info(circuitBreaker.name() + " executed when the circuit is opened:{}", v.getMessage()); + if (v instanceof HalfOpenCircuitException) { + log.info(circuitBreaker.name() + " half open circuit"); + } else if (v instanceof OpenCircuitException) { + log.info(circuitBreaker.name() + " open circuit"); + } else if (v instanceof NoStackTraceThrowable) { + log.info(circuitBreaker.name() + " close circuit"); + } + return "3"; + }, ar -> { + // Do something with the result + log.info(circuitBreaker.name() + " interface failed result.{} ", ar); +// String + if (StringUtils.equals(ar.result(), "1") == false) { + end(proxyRequest, 10016); + } + // Throwable +// if(ar.result() != null) { +// end(proxyRequest, 502); +// } + }); + }); + } + + private Future breaker(CircuitBreaker circuitBreaker, ProxyRequest proxyRequest) { + SocketAddress socketAddress = ProxyTool.resolveOriginAddress(proxyRequest.proxiedRequest()); + return Future.future(p -> { + circuitBreaker.executeWithFallback(promise -> { + mainWebClient.post(socketAddress.port(), socketAddress.host(), proxyRequest.getURI()) + .putHeaders(proxyRequest.headers()).sendJson(ctx.getBodyAsString(), h -> { + if (h.succeeded()) { + log.info("==========uri:{},response http code:{}", proxyRequest.getURI(), + h.result().statusCode()); + if (h.result().statusCode() == 200) { + // promise.complete(); + promise.complete("1"); + // 释放资源 + proxyRequest.release(); + JsonObject responseData = h.result().bodyAsJsonObject(); + log.info("responseData:{}", responseData); + ProxyResponse proxyResponse = proxyRequest.response().setStatusCode(200) + .putHeader("content-type", "application/json") + .setBody(Body.body(responseData.toBuffer())); + p.complete(proxyResponse); + } else { + promise.fail("2"); + } + } else { + promise.fail("2"); + } + }); + }, v -> { + // 需要传递当前状态half-open , close, 还是统计失败次数 + log.info(circuitBreaker.name() + " executed when the circuit is opened:{}", v.getMessage()); + if (v instanceof HalfOpenCircuitException) { + log.info(circuitBreaker.name() + " half open circuit"); + } else if (v instanceof OpenCircuitException) { + log.info(circuitBreaker.name() + " open circuit"); + } else if (v instanceof NoStackTraceThrowable) { + log.info(circuitBreaker.name() + " close circuit"); + } + return "3"; + }, ar -> { + log.info(circuitBreaker.name() + " interface failed result.{} ", ar); + if (StringUtils.equals(ar.result(), "1") == false) { + end(proxyRequest, 10016); + } + }); + }); + } + + private Future security(CircuitBreaker circuitBreaker, ProxyRequest proxyRequest) { + String appCode = proxyRequest.headers().get(AppConfigHandle.getAppCodeHeaderKey()); + SocketAddress socketAddress = ProxyTool.resolveOriginAddress(proxyRequest.proxiedRequest()); + return Future.future(p -> { + mainWebClient.post(socketAddress.port(), socketAddress.host(), proxyRequest.getURI()) + .putHeaders(proxyRequest.headers()).sendJson(bodyDecrypt(ctx.getBodyAsString(), appCode), h -> { + if (h.succeeded()) { + log.info("==========uri:{},response http code:{}", proxyRequest.getURI(), + h.result().statusCode()); + if (h.result().statusCode() == 200) { // 释放资源 proxyRequest.release(); JsonObject responseData = h.result().bodyAsJsonObject(); log.info("responseData:{}", responseData); // 加密 - String dataStr = bodyEncrypt(responseData.toString(), sacAppHeaderKey); + String dataStr = bodyEncrypt(responseData.toString(), appCode); log.info("aesEncrypt dataStr:{}", dataStr); Buffer buffer = Buffer.buffer(dataStr); ProxyResponse proxyResponse = proxyRequest.response().setStatusCode(200) .putHeader("content-type", "application/json").setBody(Body.body(buffer)); p.complete(proxyResponse); - } else { - end(proxyRequest, 502); } - }); - }); + } else { + log.info("interface retrun error.{}", proxyRequest.getURI()); + end(proxyRequest, 502); + } + }); + }); + } + + private Future sendProxyRequest(ProxyRequest proxyRequest) { + // 判断
+ // 1、是否配置全局加解密.
+ // 2、apiCode 配置熔断 + String appCode = proxyRequest.headers().get(AppConfigHandle.getAppCodeHeaderKey()); + String apiCode = proxyRequest.headers().get(AppConfigHandle.getApiCodeHeaderKey()); + String keyCircuitBreaker = appCode + ":" + apiCode + ":" + "CIRCUIT_BREAKER"; + + // 熔断 + CircuitBreaker circuitBreaker = AppConfigHandle.getApiCodeCircuitBreaker(keyCircuitBreaker); + boolean isDataSecurity = AppConfigHandle.isDataSecurity(appCode); + if (isDataSecurity || circuitBreaker != null) { + try { + if (isDataSecurity && circuitBreaker != null) { + return breakerAndSecurity(circuitBreaker, proxyRequest); + } else if (isDataSecurity) { + return security(circuitBreaker, proxyRequest); + } else if (circuitBreaker != null) { + return breaker(circuitBreaker, proxyRequest); + } else { + log.info("not match any condition.appCode:{},apiCode:{}", appCode, apiCode); + throw new HttpException(10013); + } + } catch (Exception e) { + e.printStackTrace(); + throw new HttpException(10014); + } } else { Future f = resolveOrigin(proxyRequest.proxiedRequest()); f.onFailure(err -> { + log.info("error:{}", err); // Should this be done here ? I don't think so HttpServerRequest proxiedRequest = proxyRequest.proxiedRequest(); proxiedRequest.resume(); @@ -287,7 +422,6 @@ public class ReverseProxy implements HttpProxy { end(proxyRequest, 502); }); }); - return f.compose(a -> sendProxyRequest(proxyRequest, a)); } @@ -316,28 +450,28 @@ public class ReverseProxy implements HttpProxy { return sendResponse(); } - private String bodyEncrypt(String body, String sacAppHeaderKey) { - DataSecurity dataSecurity = AppConfigServiceImpl.getAppConfig(sacAppHeaderKey).getDataSecurity(); + private String bodyEncrypt(String body, String appCode) { + DataSecurity dataSecurity = AppConfigHandle.getAppConfig(appCode).getDataSecurity(); switch (dataSecurity.getAlgorithm()) { case "AES": return MainSecurity.aesEncrypt(body, dataSecurity.getPrivateKey()); default: break; } - log.info(" appcode:{}, encrypt key config is error.", sacAppHeaderKey); - throw new HttpException(10001); + log.info(" appCode:{}, encrypt key config is error.", appCode); + throw new HttpException(10011); } - private String bodyDecrypt(String body, String sacAppHeaderKey) { - DataSecurity dataSecurity = AppConfigServiceImpl.getAppConfig(sacAppHeaderKey).getDataSecurity(); + private String bodyDecrypt(String body, String appCode) { + DataSecurity dataSecurity = AppConfigHandle.getAppConfig(appCode).getDataSecurity(); switch (dataSecurity.getAlgorithm()) { case "AES": return MainSecurity.aesDecrypt(body, dataSecurity.getPrivateKey()); default: break; } - log.info(" appcode:{}, decrypt key config is error.", sacAppHeaderKey); - throw new HttpException(10001); + log.info(" appCode:{}, decrypt key config is error.", appCode); + throw new HttpException(10011); } } } diff --git a/sf-vertx/src/test/java/com/sf/vertx/TestCaffeine.java b/sf-vertx/src/test/java/com/sf/vertx/TestCaffeine.java new file mode 100644 index 0000000..d0b60d4 --- /dev/null +++ b/sf-vertx/src/test/java/com/sf/vertx/TestCaffeine.java @@ -0,0 +1,33 @@ +package com.sf.vertx; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TestCaffeine { + + @Test + public void init() { + Cache cache = Caffeine.newBuilder() + .expireAfterWrite(10, TimeUnit.SECONDS) + //.expireAfterAccess(1, TimeUnit.SECONDS) + //.maximumSize(10) + .build(); + cache.put("hello","1"); + try { + Thread.sleep(8000); + } catch (InterruptedException e) { + } + + Object ifPresent = cache.getIfPresent("hello"); + log.info("data:{}", ifPresent); + ifPresent = cache.getIfPresent("hello"); + log.info("data:{}", ifPresent); + } +} diff --git a/sf-vertx/src/test/java/com/sf/vertx/TestCircuitBreaker.java b/sf-vertx/src/test/java/com/sf/vertx/TestCircuitBreaker.java index b3a4a3a..140d55d 100644 --- a/sf-vertx/src/test/java/com/sf/vertx/TestCircuitBreaker.java +++ b/sf-vertx/src/test/java/com/sf/vertx/TestCircuitBreaker.java @@ -1,7 +1,7 @@ package com.sf.vertx; import com.sf.vertx.api.pojo.VertxConfig; -import com.sf.vertx.service.impl.AppConfigServiceImpl; +import com.sf.vertx.handle.AppConfigHandle; import io.vertx.circuitbreaker.CircuitBreaker; import io.vertx.circuitbreaker.CircuitBreakerOptions; @@ -17,7 +17,7 @@ public class TestCircuitBreaker { private static int port; public static void main(String[] args) { - VertxConfig vertxConfig = AppConfigServiceImpl.getVertxConfig(); + VertxConfig vertxConfig = AppConfigHandle.getVertxConfig(); // TODO 编解码线程池,后面优化协程等方式 VertxOptions vertxOptions = new VertxOptions();