diff --git a/script/soul.sql b/script/soul.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/soul-admin/src/main/java/org/dromara/soul/admin/controller/SoulClientController.java b/soul-admin/src/main/java/org/dromara/soul/admin/controller/SoulClientController.java index 5fa23b9cfcda..b0feb8cf9856 100644 --- a/soul-admin/src/main/java/org/dromara/soul/admin/controller/SoulClientController.java +++ b/soul-admin/src/main/java/org/dromara/soul/admin/controller/SoulClientController.java @@ -67,7 +67,7 @@ public String registerSpringMvc(@RequestBody final SpringMvcRegisterDTO springMv public String registerSpringCloud(@RequestBody final SpringCloudRegisterDTO springCloudRegisterDTO) { return soulClientRegisterService.registerSpringCloud(springCloudRegisterDTO); } - + /** * Register dubbo string. * @@ -100,4 +100,15 @@ public String registerSofaRpc(@RequestBody final MetaDataDTO metaDataDTO) { public String registerTarsRpc(@RequestBody final MetaDataDTO metaDataDTO) { return soulClientRegisterService.registerTars(metaDataDTO); } + + /** + * Register spring mvc string. + * + * @param grpcMetaDataDTO the spring mvc register dto + * @return the string + */ + @PostMapping("/grpc-register") + public String registerGrpc(@RequestBody final MetaDataDTO grpcMetaDataDTO) { + return soulClientRegisterService.registerGrpc(grpcMetaDataDTO); + } } diff --git a/soul-admin/src/main/java/org/dromara/soul/admin/service/SoulClientRegisterService.java b/soul-admin/src/main/java/org/dromara/soul/admin/service/SoulClientRegisterService.java index 74ae4021b407..772533c0fbdc 100644 --- a/soul-admin/src/main/java/org/dromara/soul/admin/service/SoulClientRegisterService.java +++ b/soul-admin/src/main/java/org/dromara/soul/admin/service/SoulClientRegisterService.java @@ -25,7 +25,7 @@ * The interface Soul client service. */ public interface SoulClientRegisterService { - + /** * Register http string. * @@ -33,7 +33,7 @@ public interface SoulClientRegisterService { * @return the string */ String registerSpringMvc(SpringMvcRegisterDTO springMvcRegisterDTO); - + /** * Register spring cloud string. * @@ -41,7 +41,7 @@ public interface SoulClientRegisterService { * @return the string */ String registerSpringCloud(SpringCloudRegisterDTO springCloudRegisterDTO); - + /** * Register rpc string. * @@ -65,4 +65,12 @@ public interface SoulClientRegisterService { * @return the string */ String registerTars(MetaDataDTO metaDataDTO); + + /** + * Register grpc string. + * + * @param metaDataDTO the meta data dto + * @return the string + */ + String registerGrpc(MetaDataDTO metaDataDTO); } diff --git a/soul-admin/src/main/java/org/dromara/soul/admin/service/impl/SoulClientRegisterServiceImpl.java b/soul-admin/src/main/java/org/dromara/soul/admin/service/impl/SoulClientRegisterServiceImpl.java index 22f16674d709..c1ed39332057 100644 --- a/soul-admin/src/main/java/org/dromara/soul/admin/service/impl/SoulClientRegisterServiceImpl.java +++ b/soul-admin/src/main/java/org/dromara/soul/admin/service/impl/SoulClientRegisterServiceImpl.java @@ -89,8 +89,6 @@ public class SoulClientRegisterServiceImpl implements SoulClientRegisterService private final PluginMapper pluginMapper; - - /** * Instantiates a new Meta data service. * @@ -210,6 +208,15 @@ public String registerTars(final MetaDataDTO dto) { return SoulResultMessage.SUCCESS; } + @Override + public String registerGrpc(final MetaDataDTO dto) { + MetaDataDO exist = metaDataMapper.findByPath(dto.getPath()); + saveOrUpdateMetaData(exist, dto); + String selectorId = handlerGrpcSelector(dto); + handlerGrpcRule(selectorId, dto, exist); + return SoulResultMessage.SUCCESS; + } + private String handlerTarsSelector(final MetaDataDTO metaDataDTO) { return getString(metaDataDTO); } @@ -232,6 +239,17 @@ private void handlerSofaRule(final String selectorId, final MetaDataDTO metaData } } + private String handlerGrpcSelector(final MetaDataDTO metaDataDTO) { + return getString(metaDataDTO); + } + + private void handlerGrpcRule(final String selectorId, final MetaDataDTO metaDataDTO, final MetaDataDO exist) { + RuleDO existRule = ruleMapper.findByName(metaDataDTO.getPath()); + if (Objects.isNull(exist) || Objects.isNull(existRule)) { + registerRule(selectorId, metaDataDTO.getPath(), metaDataDTO.getRpcType(), metaDataDTO.getRuleName()); + } + } + private void saveSpringMvcMetaData(final SpringMvcRegisterDTO dto) { Timestamp currentTime = new Timestamp(System.currentTimeMillis()); MetaDataDO metaDataDO = MetaDataDO.builder() @@ -374,6 +392,9 @@ private String registerSelector(final String contextPath, final String rpcType, } else if (RpcTypeEnum.TARS.getName().equals(rpcType)) { selectorDTO.setPluginId(getPluginId(PluginEnum.TARS.getName())); selectorDTO.setHandle(appName); + } else if (RpcTypeEnum.GRPC.getName().equals(rpcType)) { + selectorDTO.setPluginId(getPluginId(PluginEnum.GRPC.getName())); + selectorDTO.setHandle(appName); } else { //is divide DivideUpstream divideUpstream = buildDivideUpstream(uri); diff --git a/soul-admin/src/main/resources/META-INF/schema.h2.sql b/soul-admin/src/main/resources/META-INF/schema.h2.sql index 6513464241b9..f3e0ad0cebfb 100644 --- a/soul-admin/src/main/resources/META-INF/schema.h2.sql +++ b/soul-admin/src/main/resources/META-INF/schema.h2.sql @@ -261,6 +261,7 @@ INSERT INTO `plugin` (`id`, `name`, `role`, `config`, `enabled`, `date_created`, INSERT INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('12','resilience4j', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10'); INSERT INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('13', 'tars', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10'); INSERT INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('14', 'context_path', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10'); +INSERT INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('15', 'grpc', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10'); /**default admin user**/ INSERT INTO `dashboard_user` (`id`, `user_name`, `password`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('1','admin','jHcpKkiDbbQh7W7hh8yQSA==', '1', '1', '2018-06-23 15:12:22', '2018-06-23 15:12:23'); diff --git a/soul-admin/src/main/resources/META-INF/schema.sql b/soul-admin/src/main/resources/META-INF/schema.sql index 2bf4603bd1a8..e38ad66eaf0c 100644 --- a/soul-admin/src/main/resources/META-INF/schema.sql +++ b/soul-admin/src/main/resources/META-INF/schema.sql @@ -264,6 +264,7 @@ INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `config`, `enabled`, `date_cr INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('12','resilience4j', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10'); INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('13', 'tars', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10'); INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('14', 'context_path', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10'); +INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('15', 'grpc', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10'); /**default admin user**/ INSERT IGNORE INTO `dashboard_user` (`id`, `user_name`, `password`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('1','admin','jHcpKkiDbbQh7W7hh8yQSA==', '1', '1', '2018-06-23 15:12:22', '2018-06-23 15:12:23'); diff --git a/soul-client/pom.xml b/soul-client/pom.xml index be320d50825e..fb314adbc2dd 100644 --- a/soul-client/pom.xml +++ b/soul-client/pom.xml @@ -33,6 +33,7 @@ soul-client-dubbo soul-client-sofa soul-client-tars + soul-client-grpc diff --git a/soul-client/soul-client-grpc/pom.xml b/soul-client/soul-client-grpc/pom.xml new file mode 100644 index 000000000000..5fbfa93b0c16 --- /dev/null +++ b/soul-client/soul-client-grpc/pom.xml @@ -0,0 +1,63 @@ + + + + + + soul-client + org.dromara + 2.2.1 + + 4.0.0 + + soul-client-grpc + + + 1.33.1 + + + + + org.dromara + soul-client-common + ${project.version} + + + org.springframework + spring-beans + provided + + + org.springframework + spring-context + provided + + + org.springframework + spring-core + provided + + + io.grpc + grpc-all + ${grpc.version} + provided + + + diff --git a/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/GrpcClientBeanPostProcessor.java b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/GrpcClientBeanPostProcessor.java new file mode 100644 index 000000000000..2a635b3ac8c8 --- /dev/null +++ b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/GrpcClientBeanPostProcessor.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.client.grpc; + +import io.grpc.BindableService; +import lombok.extern.slf4j.Slf4j; +import org.dromara.soul.client.common.utils.OkHttpTools; +import org.dromara.soul.client.common.utils.RegisterUtils; +import org.dromara.soul.client.grpc.common.annotation.SoulGrpcClient; +import org.dromara.soul.client.grpc.common.config.GrpcConfig; +import org.dromara.soul.client.grpc.common.dto.MetaDataDTO; +import org.dromara.soul.common.enums.RpcTypeEnum; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.lang.NonNull; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * The type Soul grpc client bean post processor. + * + * @author zhanglei + */ +@Slf4j +public class GrpcClientBeanPostProcessor implements BeanPostProcessor { + + private final ThreadPoolExecutor executorService; + + private final String url; + + private final GrpcConfig grpcConfig; + + private final String serviceName = "SERVICE_NAME"; + + /** + * Instantiates a new Soul client bean post processor. + * + * @param config the soul grpc config + */ + public GrpcClientBeanPostProcessor(final GrpcConfig config) { + String contextPath = config.getContextPath(); + String adminUrl = config.getAdminUrl(); + Integer port = config.getPort(); + if (contextPath == null || "".equals(contextPath) + || adminUrl == null || "".equals(adminUrl) + || port == null) { + log.error("grpc param must config contextPath, adminUrl and port"); + throw new RuntimeException("grpc param must config contextPath, adminUrl and port"); + } + this.grpcConfig = config; + url = adminUrl + "/soul-client/grpc-register"; + executorService = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()); + } + + @Override + public Object postProcessAfterInitialization(@NonNull final Object bean, @NonNull final String beanName) throws BeansException { + if (bean instanceof BindableService) { + executorService.execute(() -> handler(bean)); + } + return bean; + } + + private void handler(final Object serviceBean) { + Class clazz; + try { + clazz = serviceBean.getClass(); + } catch (Exception e) { + log.error("failed to get grpc target class"); + return; + } + Class parent = clazz.getSuperclass(); + Class classes = parent.getDeclaringClass(); + String packageName = null; + try { + Field field = classes.getField(serviceName); + field.setAccessible(true); + packageName = field.get(null).toString(); + } catch (Exception e) { + log.error(String.format("SERVICE_NAME field not found: %s", classes)); + return; + } + if (StringUtils.isEmpty(packageName)) { + log.error(String.format("grpc SERVICE_NAME can not found: %s", classes)); + return; + } + final Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(clazz); + for (Method method : methods) { + SoulGrpcClient grpcClient = method.getAnnotation(SoulGrpcClient.class); + if (Objects.nonNull(grpcClient)) { + RegisterUtils.doRegister(buildJsonParams(packageName, grpcClient, method), url, RpcTypeEnum.GRPC); + } + } + } + + private String buildJsonParams(final String packageName, final SoulGrpcClient soulGrpcClient, final Method method) { + String path = grpcConfig.getContextPath() + soulGrpcClient.path(); + String desc = soulGrpcClient.desc(); + String configRuleName = soulGrpcClient.ruleName(); + String ruleName = ("".equals(configRuleName)) ? path : configRuleName; + String methodName = method.getName(); + Class[] parameterTypesClazz = method.getParameterTypes(); + String parameterTypes = Arrays.stream(parameterTypesClazz).map(Class::getName) + .collect(Collectors.joining(",")); + MetaDataDTO grpcMetaDataDTO = MetaDataDTO.builder() + .appName(String.join(":", grpcConfig.getHost(), grpcConfig.getPort().toString())) + .serviceName(packageName) + .methodName(methodName) + .contextPath(grpcConfig.getContextPath()) + .path(path) + .ruleName(ruleName) + .pathDesc(desc) + .parameterTypes(parameterTypes) + .rpcType("grpc") + .rpcExt(buildRpcExt(soulGrpcClient)) + .enabled(soulGrpcClient.enabled()) + .build(); + return OkHttpTools.getInstance().getGson().toJson(grpcMetaDataDTO); + } + + private String buildRpcExt(final SoulGrpcClient soulGrpcClient) { + MetaDataDTO.RpcExt build = MetaDataDTO.RpcExt.builder() + .timeout(soulGrpcClient.timeout()) + .build(); + return OkHttpTools.getInstance().getGson().toJson(build); + + } +} + + diff --git a/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/annotation/SoulGrpcClient.java b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/annotation/SoulGrpcClient.java new file mode 100644 index 000000000000..5c15bb1dee4b --- /dev/null +++ b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/annotation/SoulGrpcClient.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.dromara.soul.client.grpc.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The interface Soul client. + * + * @author zhanglei + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface SoulGrpcClient { + /** + * Path string. + * + * @return the string + */ + String path(); + + /** + * Rule name string. + * + * @return the string + */ + String ruleName() default ""; + + /** + * Desc string. + * + * @return String string + */ + String desc() default ""; + + /** + * Enabled boolean. + * + * @return the boolean + */ + boolean enabled() default true; + + /** + * Timeout long. + * + * @return the timeout + */ + int timeout() default -1; +} diff --git a/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/config/GrpcConfig.java b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/config/GrpcConfig.java new file mode 100644 index 000000000000..5bb9e4065cf0 --- /dev/null +++ b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/config/GrpcConfig.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.dromara.soul.client.grpc.common.config; + +import lombok.Data; + +/** + * grpc config. + * + * @author zhanglei + */ +@Data +public class GrpcConfig { + + private String host; + + private Integer port; + + private String appName; + + private String adminUrl; + + private String contextPath; +} diff --git a/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/dto/MetaDataDTO.java b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/dto/MetaDataDTO.java new file mode 100644 index 000000000000..4db83d589706 --- /dev/null +++ b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/dto/MetaDataDTO.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.dromara.soul.client.grpc.common.dto; + +import lombok.Builder; +import lombok.Data; + +/** + * The type Meta data dto. + * + * @author xiaoyu + */ +@Data +@Builder +public class MetaDataDTO { + + private String appName; + + private String contextPath; + + private String path; + + private String pathDesc; + + private String rpcType; + + private String serviceName; + + private String methodName; + + private String ruleName; + + private String parameterTypes; + + private String rpcExt; + + private boolean enabled; + + /** + * The type Rpc ext. + */ + @Data + @Builder + public static class RpcExt { + + private String loadbalance; + + private Integer timeout; + + } + +} diff --git a/soul-common/src/main/java/org/dromara/soul/common/constant/Constants.java b/soul-common/src/main/java/org/dromara/soul/common/constant/Constants.java index 2ed677d115c9..4c2f7241304e 100644 --- a/soul-common/src/main/java/org/dromara/soul/common/constant/Constants.java +++ b/soul-common/src/main/java/org/dromara/soul/common/constant/Constants.java @@ -74,6 +74,11 @@ public interface Constants { */ String TARS_RPC_RESULT = "tars_rpc_result"; + /** + * The constant GRPC_RPC_RESULT. + */ + String GRPC_RPC_RESULT = "grpc_rpc_result"; + /** * The constant TARS_RPC_RESULT_EMPTY. */ @@ -124,6 +129,11 @@ public interface Constants { */ String TARS_PARAMS = "tars_params"; + /** + * The constant GRPC_PARAMS. + */ + String GRPC_PARAMS = "grpc_params"; + /** * The constant DECODE. */ diff --git a/soul-common/src/main/java/org/dromara/soul/common/enums/PluginEnum.java b/soul-common/src/main/java/org/dromara/soul/common/enums/PluginEnum.java index 86bd76e30049..e2247ddc9dcc 100644 --- a/soul-common/src/main/java/org/dromara/soul/common/enums/PluginEnum.java +++ b/soul-common/src/main/java/org/dromara/soul/common/enums/PluginEnum.java @@ -111,6 +111,11 @@ public enum PluginEnum { */ TARS(60, 0, "tars"), + /** + * GPRC plugin enum. + */ + GRPC(60, 0, "grpc"), + /** * Monitor plugin enum. */ diff --git a/soul-common/src/main/java/org/dromara/soul/common/enums/RpcTypeEnum.java b/soul-common/src/main/java/org/dromara/soul/common/enums/RpcTypeEnum.java index bcfd1992989d..56de03199969 100644 --- a/soul-common/src/main/java/org/dromara/soul/common/enums/RpcTypeEnum.java +++ b/soul-common/src/main/java/org/dromara/soul/common/enums/RpcTypeEnum.java @@ -72,7 +72,7 @@ public enum RpcTypeEnum { /** * grpc. */ - GRPC("grpc", false); + GRPC("grpc", true); private final String name; diff --git a/soul-examples/pom.xml b/soul-examples/pom.xml index 9554055ea55b..2c7da1514c10 100644 --- a/soul-examples/pom.xml +++ b/soul-examples/pom.xml @@ -45,6 +45,7 @@ soul-examples-eureka soul-examples-sofa soul-examples-tars + soul-examples-grpc diff --git a/soul-examples/soul-examples-grpc/pom.xml b/soul-examples/soul-examples-grpc/pom.xml new file mode 100644 index 000000000000..0eba19d95dc6 --- /dev/null +++ b/soul-examples/soul-examples-grpc/pom.xml @@ -0,0 +1,113 @@ + + + + + + soul-examples + org.dromara + 2.1.0 + + 4.0.0 + + soul-examples-grpc + + + 1.33.1 + 3.13.0 + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + org.dromara + soul-spring-boot-starter-client-grpc + ${soul.version} + + + guava + com.google.guava + + + + + + io.grpc + grpc-all + ${grpc.version} + provided + + + + + + + + kr.motd.maven + os-maven-plugin + 1.6.2 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + src/main/resources/proto + + + + + + compile + compile-custom + + + + + + + + + diff --git a/soul-examples/soul-examples-grpc/src/main/java/org/dromara/soul/examples/grpc/SoulTestGrpcApplication.java b/soul-examples/soul-examples-grpc/src/main/java/org/dromara/soul/examples/grpc/SoulTestGrpcApplication.java new file mode 100644 index 000000000000..54728eb03631 --- /dev/null +++ b/soul-examples/soul-examples-grpc/src/main/java/org/dromara/soul/examples/grpc/SoulTestGrpcApplication.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.examples.grpc; + +import io.grpc.ServerBuilder; +import io.grpc.protobuf.services.ProtoReflectionService; +import org.dromara.soul.examples.grpc.echo.EchoServiceImpl; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.io.IOException; + +/** + * SoulTestTarsApplication. + * @author tydhot + */ +@SpringBootApplication +public class SoulTestGrpcApplication { + + /** + * main. + * + * @param args args + */ + public static void main(final String[] args) throws IOException { + SpringApplication.run(SoulTestGrpcApplication.class, args); + io.grpc.Server server = ServerBuilder + .forPort(8080) + .addService(new EchoServiceImpl()) + .addService(ProtoReflectionService.newInstance()) + .build(); + server.start(); + } + +} diff --git a/soul-examples/soul-examples-grpc/src/main/java/org/dromara/soul/examples/grpc/echo/EchoServiceImpl.java b/soul-examples/soul-examples-grpc/src/main/java/org/dromara/soul/examples/grpc/echo/EchoServiceImpl.java new file mode 100644 index 000000000000..5b73620c1f9a --- /dev/null +++ b/soul-examples/soul-examples-grpc/src/main/java/org/dromara/soul/examples/grpc/echo/EchoServiceImpl.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.examples.grpc.echo; + +import echo.EchoRequest; +import echo.EchoResponse; +import echo.EchoServiceGrpc; +import echo.Trace; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.stub.StreamObserver; +import org.dromara.soul.client.grpc.common.annotation.SoulGrpcClient; +import org.springframework.stereotype.Service; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * @author zhanglei + */ +@Service +public class EchoServiceImpl extends EchoServiceGrpc.EchoServiceImplBase { + + @Override + @SoulGrpcClient(path = "/echo") + public void echo(EchoRequest request, StreamObserver responseObserver) { + System.out.println("Received: " + request.getMessage()); + + EchoResponse.Builder response = EchoResponse.newBuilder() + .setMessage("ReceivedHELLO") + .addTraces(Trace.newBuilder().setHost(getHostname()).build()); + + responseObserver.onNext(response.build()); + responseObserver.onCompleted(); + } + + private String getHostname() { + try { + return InetAddress.getLocalHost().getHostName() + "(" + InetAddress.getLocalHost().getHostAddress() + ")"; + } catch (UnknownHostException e) { + return ""; + } + } +} diff --git a/soul-examples/soul-examples-grpc/src/main/resources/application.yml b/soul-examples/soul-examples-grpc/src/main/resources/application.yml new file mode 100644 index 000000000000..973e3fa1c68b --- /dev/null +++ b/soul-examples/soul-examples-grpc/src/main/resources/application.yml @@ -0,0 +1,38 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +server: + port: 55290 + servlet: + context-path: / + address: 0.0.0.0 + +spring: + application: + name: grpc-test + +logging: + level: + root: info + org.dromara.soul: debug + path: "./logs" + +soul: + grpc: + adminUrl: http://localhost:9095 + contextPath: /grpc + appName: grpc + host: 127.0.0.1 + port: 8080 diff --git a/soul-examples/soul-examples-grpc/src/main/resources/proto/echo.proto b/soul-examples/soul-examples-grpc/src/main/resources/proto/echo.proto new file mode 100644 index 000000000000..026a31030dff --- /dev/null +++ b/soul-examples/soul-examples-grpc/src/main/resources/proto/echo.proto @@ -0,0 +1,37 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +syntax = "proto3"; + +package echo; + +option java_multiple_files = true; + + +service EchoService { + rpc echo (EchoRequest) returns (EchoResponse) {} +} + +message EchoRequest { + string message = 1; +} + +message EchoResponse { + string message = 1; + repeated Trace traces = 2; +} + +message Trace { + string host = 1; +} diff --git a/soul-plugin/pom.xml b/soul-plugin/pom.xml index b85ec3a685c4..429e75042ecf 100644 --- a/soul-plugin/pom.xml +++ b/soul-plugin/pom.xml @@ -48,6 +48,7 @@ soul-plugin-sofa soul-plugin-tars soul-plugin-context-path + soul-plugin-grpc diff --git a/soul-plugin/soul-plugin-api/src/main/java/org/dromara/soul/plugin/api/result/SoulResultEnum.java b/soul-plugin/soul-plugin-api/src/main/java/org/dromara/soul/plugin/api/result/SoulResultEnum.java index 9a6541198655..3aee73da360a 100644 --- a/soul-plugin/soul-plugin-api/src/main/java/org/dromara/soul/plugin/api/result/SoulResultEnum.java +++ b/soul-plugin/soul-plugin-api/src/main/java/org/dromara/soul/plugin/api/result/SoulResultEnum.java @@ -79,6 +79,16 @@ public enum SoulResultEnum { */ TARS_INVOKE(434, "Tars invoke error!"), + /** + * Grpc have body param soul result enum. + */ + GRPC_HAVE_BODY_PARAM(435, "grpc must have body param, please enter the JSON format in the body!"), + + /** + * Grpc client resultenum. + */ + GRPC_CLIENT_NULL(436, "grpc client is null, please check the context path!"), + /** * full selector type enum. */ diff --git a/soul-plugin/soul-plugin-global/src/main/java/org/dromara/soul/plugin/global/DefaultSoulContextBuilder.java b/soul-plugin/soul-plugin-global/src/main/java/org/dromara/soul/plugin/global/DefaultSoulContextBuilder.java index 960c01824b92..764dcb8f42eb 100644 --- a/soul-plugin/soul-plugin-global/src/main/java/org/dromara/soul/plugin/global/DefaultSoulContextBuilder.java +++ b/soul-plugin/soul-plugin-global/src/main/java/org/dromara/soul/plugin/global/DefaultSoulContextBuilder.java @@ -20,6 +20,7 @@ import java.time.LocalDateTime; import java.util.Objects; import java.util.Optional; + import org.apache.commons.lang3.StringUtils; import org.dromara.soul.common.constant.Constants; import org.dromara.soul.common.dto.MetaData; @@ -36,7 +37,7 @@ * @author xiaoyu */ public class DefaultSoulContextBuilder implements SoulContextBuilder { - + @Override public SoulContext build(final ServerWebExchange exchange) { final ServerHttpRequest request = exchange.getRequest(); @@ -47,7 +48,7 @@ public SoulContext build(final ServerWebExchange exchange) { } return transform(request, metaData); } - + /** * ServerHttpRequest transform RequestDTO . * @@ -71,6 +72,8 @@ private SoulContext transform(final ServerHttpRequest request, final MetaData me setSoulContextBySofa(soulContext, metaData); } else if (RpcTypeEnum.TARS.getName().equals(metaData.getRpcType())) { setSoulContextByTars(soulContext, metaData); + } else if (RpcTypeEnum.GRPC.getName().equals(metaData.getRpcType())) { + setSoulContextByGrpc(soulContext, metaData); } else { setSoulContextByHttp(soulContext, path); soulContext.setRpcType(RpcTypeEnum.HTTP.getName()); @@ -86,7 +89,7 @@ private SoulContext transform(final ServerHttpRequest request, final MetaData me Optional.ofNullable(request.getMethod()).ifPresent(httpMethod -> soulContext.setHttpMethod(httpMethod.name())); return soulContext; } - + private void setSoulContextByDubbo(final SoulContext soulContext, final MetaData metaData) { soulContext.setModule(metaData.getAppName()); soulContext.setMethod(metaData.getServiceName()); @@ -107,7 +110,14 @@ private void setSoulContextByTars(final SoulContext soulContext, final MetaData soulContext.setRpcType(metaData.getRpcType()); soulContext.setContextPath(metaData.getContextPath()); } - + + private void setSoulContextByGrpc(final SoulContext soulContext, final MetaData metaData) { + soulContext.setModule(metaData.getServiceName()); + soulContext.setMethod(metaData.getMethodName()); + soulContext.setRpcType(metaData.getRpcType()); + soulContext.setContextPath(metaData.getContextPath()); + } + private void setSoulContextByHttp(final SoulContext soulContext, final String path) { String contextPath = "/"; String[] splitList = StringUtils.split(path, "/"); diff --git a/soul-plugin/soul-plugin-grpc/pom.xml b/soul-plugin/soul-plugin-grpc/pom.xml new file mode 100644 index 000000000000..07767928cff1 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/pom.xml @@ -0,0 +1,64 @@ + + + + + + soul-plugin + org.dromara + 2.2.1 + + 4.0.0 + + soul-plugin-grpc + + 1.33.1 + 3.12.0 + + + + + org.dromara + soul-plugin-base + ${project.version} + + + + io.grpc + grpc-all + ${grpc.version} + + + com.google.protobuf + protobuf-java + ${protobuf-java.version} + + + org.springframework + spring-test + test + + + io.projectreactor + reactor-test + test + + + + diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/GrpcPlugin.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/GrpcPlugin.java new file mode 100644 index 000000000000..88757dbeca45 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/GrpcPlugin.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc; + +import io.grpc.CallOptions; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.dromara.soul.common.constant.Constants; +import org.dromara.soul.common.dto.MetaData; +import org.dromara.soul.common.dto.RuleData; +import org.dromara.soul.common.dto.SelectorData; +import org.dromara.soul.common.enums.PluginEnum; +import org.dromara.soul.common.enums.ResultEnum; +import org.dromara.soul.common.enums.RpcTypeEnum; +import org.dromara.soul.common.exception.SoulException; +import org.dromara.soul.plugin.api.SoulPluginChain; +import org.dromara.soul.plugin.api.context.SoulContext; +import org.dromara.soul.plugin.api.result.SoulResultEnum; +import org.dromara.soul.plugin.base.AbstractSoulPlugin; +import org.dromara.soul.plugin.base.utils.SoulResultWrap; +import org.dromara.soul.plugin.base.utils.WebFluxResultUtils; +import org.dromara.soul.plugin.grpc.cache.GrpcClientCache; +import org.dromara.soul.plugin.grpc.client.SoulGrpcClient; +import org.dromara.soul.plugin.grpc.proto.SoulGrpcResponse; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +/** + * The type grpc plugin. + * + * @author xiaoyu(Myth) + */ +@Slf4j +public class GrpcPlugin extends AbstractSoulPlugin { + + @Override + protected Mono doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) { + String body = exchange.getAttribute(Constants.GRPC_PARAMS); + SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT); + assert soulContext != null; + MetaData metaData = exchange.getAttribute(Constants.META_DATA); + if (!checkMetaData(metaData)) { + assert metaData != null; + log.error(" path is :{}, meta data have error.... {}", soulContext.getPath(), metaData.toString()); + exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); + Object error = SoulResultWrap.error(SoulResultEnum.META_DATA_ERROR.getCode(), SoulResultEnum.META_DATA_ERROR.getMsg(), null); + return WebFluxResultUtils.result(exchange, error); + } + if (StringUtils.isNoneBlank(metaData.getParameterTypes()) && StringUtils.isBlank(body)) { + exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); + Object error = SoulResultWrap.error(SoulResultEnum.GRPC_HAVE_BODY_PARAM.getCode(), SoulResultEnum.GRPC_HAVE_BODY_PARAM.getMsg(), null); + return WebFluxResultUtils.result(exchange, error); + } + + final SoulGrpcClient client = GrpcClientCache.getGrpcClient(metaData.getContextPath()); + if (Objects.isNull(client)) { + exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); + Object error = SoulResultWrap.error(SoulResultEnum.GRPC_CLIENT_NULL.getCode(), SoulResultEnum.GRPC_CLIENT_NULL.getMsg(), null); + return WebFluxResultUtils.result(exchange, error); + } + CompletableFuture result = client.call(metaData, CallOptions.DEFAULT, body); + return Mono.fromFuture(result.thenApply(ret -> { + exchange.getAttributes().put(Constants.GRPC_RPC_RESULT, ret.getResult()); + exchange.getAttributes().put(Constants.CLIENT_RESPONSE_RESULT_TYPE, ResultEnum.SUCCESS.getName()); + return ret; + })).onErrorMap(SoulException::new).then(chain.execute(exchange)); + } + + /** + * acquire plugin name. + * + * @return plugin name. + */ + @Override + public String named() { + return PluginEnum.GRPC.getName(); + } + + /** + * plugin is execute. + * + * @param exchange the current server exchange + * @return default false. + */ + @Override + public Boolean skip(final ServerWebExchange exchange) { + final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT); + assert soulContext != null; + return !Objects.equals(soulContext.getRpcType(), RpcTypeEnum.GRPC.getName()); + } + + @Override + public int getOrder() { + return PluginEnum.GRPC.getCode(); + } + + private boolean checkMetaData(final MetaData metaData) { + return null != metaData && !StringUtils.isBlank(metaData.getMethodName()) && !StringUtils.isBlank(metaData.getServiceName()); + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/cache/ApplicationConfigCache.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/cache/ApplicationConfigCache.java new file mode 100644 index 000000000000..8fcf99ad1327 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/cache/ApplicationConfigCache.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.cache; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.cache.Weigher; +import lombok.extern.slf4j.Slf4j; +import org.dromara.soul.common.dto.MetaData; +import org.dromara.soul.common.exception.SoulException; +import org.dromara.soul.plugin.grpc.resolver.SoulServiceInstance; +import org.dromara.soul.plugin.grpc.resolver.SoulServiceInstanceLists; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + +/** + * Grpc config cache. + * + * @author zhanglei + */ +@Slf4j +public final class ApplicationConfigCache { + + private final int maxCount = 50000; + + private final LoadingCache cache = CacheBuilder.newBuilder() + .maximumWeight(maxCount) + .weigher((Weigher) (string, referenceConfig) -> getSize()) + .build(new CacheLoader() { + @Override + public SoulServiceInstanceLists load(final String key) { + return new SoulServiceInstanceLists(new CopyOnWriteArrayList<>(), key); + } + }); + + private final Map listener = new ConcurrentHashMap<>(); + + private ApplicationConfigCache() { + } + + private int getSize() { + return (int) cache.size(); + } + + /** + * Get soulServiceInstanceList. + * + * @param path path + * @return SoulServiceInstanceLists instances + */ + public SoulServiceInstanceLists get(final String path) { + try { + return cache.get(path); + } catch (ExecutionException e) { + throw new SoulException(e.getCause()); + } + } + + /** + * Init prx. + * + * @param metaData metaData + */ + public void initPrx(final MetaData metaData) { + try { + SoulServiceInstanceLists soulServiceInstances = cache.get(metaData.getContextPath()); + List instances = soulServiceInstances.getSoulServiceInstances(); + String[] ipAndPort = metaData.getAppName().split(":"); + instances.add(new SoulServiceInstance(ipAndPort[0], Integer.valueOf(ipAndPort[1]))); + Consumer consumer = listener.get(metaData.getContextPath()); + if (Objects.nonNull(consumer)) { + consumer.accept(System.currentTimeMillis()); + } + } catch (ExecutionException e) { + throw new SoulException(e.getCause()); + } + } + + /** + * invalidate client. + * + * @param metaData metaData + */ + public void invalidate(final MetaData metaData) { + cache.invalidate(metaData.getContextPath()); + listener.remove(metaData.getContextPath()); + GrpcClientCache.removeClient(metaData.getContextPath()); + } + + /** + * Refresh. + * + * @param key contextPath + * @param consumer consumer + */ + public void watch(final String key, final Consumer consumer) { + listener.put(key, consumer); + } + + /** + * Gets instance. + * + * @return the instance + */ + public static ApplicationConfigCache getInstance() { + return ApplicationConfigCacheInstance.INSTANCE; + } + + /** + * The type Application config cache instance. + */ + static class ApplicationConfigCacheInstance { + /** + * The Instance. + */ + static final ApplicationConfigCache INSTANCE = new ApplicationConfigCache(); + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/cache/GrpcClientCache.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/cache/GrpcClientCache.java new file mode 100644 index 000000000000..2c542148804c --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/cache/GrpcClientCache.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.cache; + +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.dromara.soul.plugin.grpc.client.GrpcClientBuilder; +import org.dromara.soul.plugin.grpc.client.SoulGrpcClient; + +import java.util.Map; +import java.util.Objects; + +/** + * The Grpc client cache. + * + * @author zhanglei + */ +@Slf4j +public class GrpcClientCache { + + private static final Map CLIENT_CACHE = Maps.newConcurrentMap(); + + static { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + for (Map.Entry entry : CLIENT_CACHE.entrySet()) { + SoulGrpcClient grpcClient = entry.getValue(); + grpcClient.close(); + } + CLIENT_CACHE.clear(); + })); + } + + /** + * Init client. + * + * @param contextPath contextPath + */ + public static void initGrpcClient(final String contextPath) { + CLIENT_CACHE.computeIfAbsent(contextPath, s -> { + return GrpcClientBuilder.buildClient(contextPath); + }); + } + + /** + * Get the client. + * + * @param contextPath contextPath + * @return SoulGrpcClient oulGrpcClient + */ + public static SoulGrpcClient getGrpcClient(final String contextPath) { + return CLIENT_CACHE.get(contextPath); + } + + /** + * Remove client. + * + * @param contextPath contextPath + */ + public static void removeClient(final String contextPath) { + SoulGrpcClient grpcClient = CLIENT_CACHE.remove(contextPath); + if (Objects.nonNull(grpcClient)) { + grpcClient.close(); + } + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/client/GrpcClientBuilder.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/client/GrpcClientBuilder.java new file mode 100644 index 000000000000..7224e16931d2 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/client/GrpcClientBuilder.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.client; + +import io.grpc.LoadBalancerRegistry; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.NameResolverRegistry; +import org.dromara.soul.plugin.grpc.loadbalance.LoadBalancerStrategy; +import org.dromara.soul.plugin.grpc.loadbalance.RandomLoadBalancerProvider; +import org.dromara.soul.plugin.grpc.loadbalance.RoundRobinLoadBalancerProvider; +import org.dromara.soul.plugin.grpc.resolver.SoulNameResolverProvider; + +/** + * Grpc client Builder. + * + * @author zhanglei + */ +public class GrpcClientBuilder { + + static { + LoadBalancerRegistry.getDefaultRegistry().register(new RandomLoadBalancerProvider()); + LoadBalancerRegistry.getDefaultRegistry().register(new RoundRobinLoadBalancerProvider()); + NameResolverRegistry.getDefaultRegistry().register(new SoulNameResolverProvider()); + } + + /** + * Build the client. + * + * @param contextPath contextPath + * @return SoulGrpcClient soulGrpcClient + */ + public static SoulGrpcClient buildClient(final String contextPath) { + ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget(contextPath) + .defaultLoadBalancingPolicy(LoadBalancerStrategy.Random.getStrategy()) + .usePlaintext() + .maxInboundMessageSize(100 * 1024 * 1024) + .disableRetry(); + ManagedChannel channel = builder.build(); + channel.getState(true); + return new SoulGrpcClient(channel); + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/client/SoulGrpcClient.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/client/SoulGrpcClient.java new file mode 100644 index 000000000000..3b5d19cb41a8 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/client/SoulGrpcClient.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.client; + +import java.io.Closeable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.util.JsonFormat; +import io.grpc.CallOptions; +import io.grpc.ClientCall; +import io.grpc.ManagedChannel; +import io.grpc.MethodDescriptor; +import io.grpc.stub.StreamObserver; +import lombok.extern.slf4j.Slf4j; +import org.dromara.soul.common.dto.MetaData; +import org.dromara.soul.plugin.grpc.proto.DynamicMessageMarshaller; +import org.dromara.soul.plugin.grpc.proto.MessageWriter; +import org.dromara.soul.plugin.grpc.proto.SoulGrpcCallRequest; +import org.dromara.soul.plugin.grpc.proto.SoulGrpcResponse; +import org.dromara.soul.plugin.grpc.proto.CompleteObserver; +import org.dromara.soul.plugin.grpc.proto.CompositeStreamObserver; +import org.dromara.soul.plugin.grpc.reflection.SoulGrpcReflectionClient; +import org.dromara.soul.plugin.grpc.resolver.ServiceResolver; + +import static io.grpc.stub.ClientCalls.asyncUnaryCall; +import static io.grpc.stub.ClientCalls.asyncServerStreamingCall; +import static io.grpc.stub.ClientCalls.asyncClientStreamingCall; +import static io.grpc.stub.ClientCalls.asyncBidiStreamingCall; + + +/** + * The Soul grpc client. + * + * @author zhanglei + */ +@Slf4j +public class SoulGrpcClient implements Closeable { + + private final ManagedChannel channel; + + private final SoulGrpcReflectionClient reflectionClient; + + public SoulGrpcClient(final ManagedChannel channel) { + this.channel = channel; + this.reflectionClient = SoulGrpcReflectionClient.create(channel); + } + + /** + * Grpc call. + * + * @param metaData metadata + * @param callOptions callOptions + * @param requestJsons requestJsons + * @return CompletableFuture future + */ + public CompletableFuture call(final MetaData metaData, final CallOptions callOptions, final String requestJsons) { + DescriptorProtos.FileDescriptorSet fileDescriptorSet = reflectionClient.resolveService(metaData.getServiceName()); + if (fileDescriptorSet == null) { + return null; + } + ServiceResolver serviceResolver = ServiceResolver.fromFileDescriptorSet(fileDescriptorSet); + Descriptors.MethodDescriptor methodDescriptor = serviceResolver.resolveServiceMethod(metaData); + JsonFormat.TypeRegistry registry = JsonFormat.TypeRegistry.newBuilder().add(serviceResolver.listMessageTypes()).build(); + DynamicMessage requestMessages = reflectionClient.parseToMessages(registry, methodDescriptor.getInputType(), requestJsons); + SoulGrpcResponse soulGrpcResponse = new SoulGrpcResponse(); + StreamObserver streamObserver = MessageWriter.newInstance(registry, soulGrpcResponse); + SoulGrpcCallRequest callParams = SoulGrpcCallRequest.builder() + .methodDescriptor(methodDescriptor) + .channel(channel) + .callOptions(callOptions) + .requests(requestMessages) + .responseObserver(streamObserver) + .build(); + try { + this.invoke(callParams).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException("Caught exception while waiting for rpc", e); + } + return CompletableFuture.completedFuture(soulGrpcResponse); + } + + /** + * Grpc call. + * + * @param callParams callParams + * @return ListenableFuture future + */ + public ListenableFuture invoke(final SoulGrpcCallRequest callParams) { + MethodDescriptor.MethodType methodType = reflectionClient.fetchMethodType(callParams.getMethodDescriptor()); + DynamicMessage request = callParams.getRequests(); + StreamObserver responseObserver = callParams.getResponseObserver(); + CompleteObserver doneObserver = new CompleteObserver<>(); + StreamObserver compositeObserver = CompositeStreamObserver.of(responseObserver, doneObserver); + StreamObserver requestObserver; + switch (methodType) { + case UNARY: + asyncUnaryCall(createCall(callParams), request, compositeObserver); + return doneObserver.getCompletionFuture(); + case SERVER_STREAMING: + asyncServerStreamingCall(createCall(callParams), request, compositeObserver); + return doneObserver.getCompletionFuture(); + case CLIENT_STREAMING: + requestObserver = asyncClientStreamingCall(createCall(callParams), compositeObserver); + requestObserver.onCompleted(); + return doneObserver.getCompletionFuture(); + case BIDI_STREAMING: + requestObserver = asyncBidiStreamingCall(createCall(callParams), compositeObserver); + requestObserver.onCompleted(); + return doneObserver.getCompletionFuture(); + default: + log.info("Unknown methodType:{}", methodType); + return null; + } + } + + @Override + public void close() { + this.channel.shutdown(); + this.reflectionClient.getFileDescriptorCache().clear(); + } + + private ClientCall createCall(final SoulGrpcCallRequest callParams) { + return callParams.getChannel().newCall(createGrpcMethodDescriptor(callParams.getMethodDescriptor()), + callParams.getCallOptions()); + } + + private io.grpc.MethodDescriptor createGrpcMethodDescriptor(final Descriptors.MethodDescriptor descriptor) { + return io.grpc.MethodDescriptor.newBuilder() + .setType(reflectionClient.fetchMethodType(descriptor)) + .setFullMethodName(reflectionClient.fetchFullMethodName(descriptor)) + .setRequestMarshaller(new DynamicMessageMarshaller(descriptor.getInputType())) + .setResponseMarshaller(new DynamicMessageMarshaller(descriptor.getOutputType())) + .build(); + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/AbstractLoadBalancer.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/AbstractLoadBalancer.java new file mode 100644 index 000000000000..0ffdcb37cd55 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/AbstractLoadBalancer.java @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.loadbalance; + +import io.grpc.ConnectivityState; +import io.grpc.EquivalentAddressGroup; +import io.grpc.LoadBalancer; +import io.grpc.Status; +import lombok.extern.slf4j.Slf4j; +import org.dromara.soul.plugin.grpc.loadbalance.picker.AbstractReadyPicker; +import org.dromara.soul.plugin.grpc.loadbalance.picker.AbstractPicker; +import org.dromara.soul.plugin.grpc.loadbalance.picker.EmptyPicker; +import io.grpc.Attributes; +import io.grpc.ConnectivityStateInfo; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.HashMap; +import java.util.List; +import java.util.HashSet; +import java.util.Collection; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.ConnectivityState.CONNECTING; +import static io.grpc.ConnectivityState.IDLE; +import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; +import static io.grpc.ConnectivityState.SHUTDOWN; +import static io.grpc.ConnectivityState.READY; + +/** + * LoadBalancer. + * + * @author zhanglei + */ +@Slf4j +public abstract class AbstractLoadBalancer extends LoadBalancer { + + private static final Status EMPTY_OK = Status.OK.withDescription("no subchannels ready"); + + private final Helper helper; + + private final AtomicReference serviceName = new AtomicReference<>(); + + private final Map subchannels = new ConcurrentHashMap<>(); + + private ConnectivityState currentState; + + private AbstractPicker currentPicker = new EmptyPicker(EMPTY_OK); + + public AbstractLoadBalancer(final Helper helper) { + this.helper = checkNotNull(helper, "helper"); + } + + private String getServiceName() { + return serviceName.get(); + } + + private void setAttribute(final Attributes attributes) { + this.serviceName.compareAndSet(null, attributes.get(GrpcAttributeUtils.appName()).toString()); + } + + @Override + public void handleResolvedAddresses(final ResolvedAddresses resolvedAddresses) { + setAttribute(resolvedAddresses.getAttributes()); + Set currentAddrs = subchannels.keySet(); + Map latestAddrs = stripAttrs(resolvedAddresses.getAddresses()); + Set removedAddrs = setsDifference(currentAddrs, latestAddrs.keySet()); + for (Map.Entry latestEntry : latestAddrs.entrySet()) { + EquivalentAddressGroup strippedAddressGroup = latestEntry.getKey(); + EquivalentAddressGroup originalAddressGroup = latestEntry.getValue(); + Subchannel subchannel; + Subchannel existingSubchannel = subchannels.get(strippedAddressGroup); + if (Objects.nonNull(existingSubchannel)) { + subchannel = existingSubchannel; + SubChannels.updateAttributes(existingSubchannel, originalAddressGroup.getAttributes()); + } else { + subchannel = SubChannels.createSubChannel(helper, strippedAddressGroup, originalAddressGroup.getAttributes()); + subchannel.start(state -> processSubchannelState(subchannel, state)); + subchannels.put(strippedAddressGroup, subchannel); + } + subchannel.requestConnection(); + } + List removedSubchannels = new ArrayList<>(); + for (EquivalentAddressGroup addressGroup : removedAddrs) { + removedSubchannels.add(subchannels.remove(addressGroup)); + } + updateBalancingState(); + for (Subchannel removedSubchannel : removedSubchannels) { + shutdownSubchannel(removedSubchannel); + } + } + + private void processSubchannelState(final Subchannel subchannel, final ConnectivityStateInfo stateInfo) { + if (subchannels.get(stripAttrs(subchannel.getAddresses())) != subchannel) { + return; + } + if (stateInfo.getState() == IDLE) { + subchannel.requestConnection(); + log.info("AbstractLoadBalancer.handleSubchannelState, current state:IDLE, subchannel.requestConnection()."); + } + final ConnectivityStateInfo originStateInfo = SubChannels.getStateInfo(subchannel); + if (originStateInfo.getState().equals(TRANSIENT_FAILURE)) { + if (stateInfo.getState().equals(CONNECTING) || stateInfo.getState().equals(IDLE)) { + return; + } + } + SubChannels.setStateInfo(subchannel, stateInfo); + updateBalancingState(); + } + + private Map stripAttrs(final List groupList) { + Map addrs = new HashMap<>(groupList.size() * 2); + for (EquivalentAddressGroup group : groupList) { + addrs.put(stripAttrs(group), group); + } + return addrs; + } + + private static EquivalentAddressGroup stripAttrs(final EquivalentAddressGroup eag) { + return new EquivalentAddressGroup(eag.getAddresses()); + } + + private Set setsDifference(final Set a, final Set b) { + Set aCopy = new HashSet<>(a); + aCopy.removeAll(b); + return aCopy; + } + + @Override + public void shutdown() { + for (Subchannel subchannel : subchannels.values()) { + shutdownSubchannel(subchannel); + } + } + + private void shutdownSubchannel(final Subchannel subchannel) { + subchannel.shutdown(); + SubChannels.setStateInfo(subchannel, ConnectivityStateInfo.forNonError(SHUTDOWN)); + } + + @Override + public void handleNameResolutionError(final Status error) { + updateBalancingState(TRANSIENT_FAILURE, + currentPicker instanceof AbstractReadyPicker ? currentPicker : new EmptyPicker(error)); + } + + /** + * Updates picker with the list of active subchannels (state == READY). + */ + private void updateBalancingState() { + final List activeList = subchannels.values() + .stream() + .filter(r -> SubChannels.getStateInfo(r).getState() == READY) + .collect(Collectors.toList()); + if (activeList.isEmpty()) { + // No READY subchannels + boolean isConnecting = false; + Status aggStatus = EMPTY_OK; + for (Subchannel subchannel : getSubchannels()) { + ConnectivityStateInfo stateInfo = SubChannels.getStateInfo(subchannel); + if (stateInfo.getState() == CONNECTING || stateInfo.getState() == IDLE) { + isConnecting = true; + } + if (aggStatus == EMPTY_OK || !aggStatus.isOk()) { + aggStatus = stateInfo.getStatus(); + } + } + updateBalancingState(isConnecting ? CONNECTING : TRANSIENT_FAILURE, new EmptyPicker(aggStatus)); + } else { + updateBalancingState(READY, newPicker(new ArrayList<>(subchannels.values()))); + } + } + + private void updateBalancingState(final ConnectivityState state, final AbstractPicker picker) { + if (state == currentState && picker.isEquivalentTo(currentPicker)) { + return; + } + helper.updateBalancingState(state, picker); + currentState = state; + currentPicker = picker; + log.info("AbstractPicker update, serviceName:{}, all subchannels:{}, state:{}", serviceName, picker.getSubchannelsInfo(), state); + } + + private Collection getSubchannels() { + return subchannels.values(); + } + + /** + * Create new picker. + * + * @param list all subchannels + * @return ReadyPicker + */ + protected abstract AbstractReadyPicker newPicker(List list); +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/GrpcAttributeUtils.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/GrpcAttributeUtils.java new file mode 100644 index 000000000000..069baa86c386 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/GrpcAttributeUtils.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.loadbalance; + +import io.grpc.Attributes; + +/** + * GrpcAttributeUtils. + * + * @author zhanglei + */ +public class GrpcAttributeUtils { + + /** + * The soul instance appname. + */ + private static Attributes.Key appName = Attributes.Key.create("appName"); + + /** + * AppName. + * + * @return key + */ + public static Attributes.Key appName() { + return appName; + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/LoadBalancerStrategy.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/LoadBalancerStrategy.java new file mode 100644 index 000000000000..4be818fc8d36 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/LoadBalancerStrategy.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.loadbalance; + +import lombok.Getter; + +/** + * LoadBalancerStrategy. + * + * @author zhanglei + */ +public enum LoadBalancerStrategy { + + Random("random"), + RoundRobin("round-robin"); + + @Getter + private final String strategy; + + LoadBalancerStrategy(final String strategy) { + this.strategy = strategy; + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/RandomLoadBalancerProvider.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/RandomLoadBalancerProvider.java new file mode 100644 index 000000000000..f676749856f9 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/RandomLoadBalancerProvider.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.loadbalance; + +import io.grpc.LoadBalancer; +import io.grpc.LoadBalancerProvider; +import lombok.extern.slf4j.Slf4j; +import org.dromara.soul.plugin.grpc.loadbalance.picker.AbstractReadyPicker; +import org.dromara.soul.plugin.grpc.loadbalance.picker.RandomPicker; + +import java.util.List; + +/** + * RandomLoadBalancerProvider. + * + * @author raphael + */ +@Slf4j +public class RandomLoadBalancerProvider extends LoadBalancerProvider { + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public int getPriority() { + return 6; + } + + @Override + public String getPolicyName() { + return LoadBalancerStrategy.Random.getStrategy(); + } + + @Override + public LoadBalancer newLoadBalancer(final LoadBalancer.Helper helper) { + return new AbstractLoadBalancer(helper) { + @Override + protected AbstractReadyPicker newPicker(final List list) { + return new RandomPicker(list); + } + }; + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/RoundRobinLoadBalancerProvider.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/RoundRobinLoadBalancerProvider.java new file mode 100644 index 000000000000..5bf0965ddf3e --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/RoundRobinLoadBalancerProvider.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.loadbalance; + +import io.grpc.LoadBalancer; +import io.grpc.LoadBalancerProvider; +import lombok.extern.slf4j.Slf4j; +import org.dromara.soul.plugin.grpc.loadbalance.picker.AbstractReadyPicker; +import org.dromara.soul.plugin.grpc.loadbalance.picker.RoundRobinPicker; + +import java.util.List; + +/** + * RoundRobinLoadBalancerProvider. + * + * @author zhanglei + */ +@Slf4j +public class RoundRobinLoadBalancerProvider extends LoadBalancerProvider { + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public int getPriority() { + return 6; + } + + @Override + public String getPolicyName() { + return LoadBalancerStrategy.RoundRobin.getStrategy(); + } + + @Override + public LoadBalancer newLoadBalancer(final LoadBalancer.Helper helper) { + return new AbstractLoadBalancer(helper) { + @Override + protected AbstractReadyPicker newPicker(final List list) { + return new RoundRobinPicker(list); + } + }; + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/SubChannels.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/SubChannels.java new file mode 100644 index 000000000000..d636ffa88ce5 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/SubChannels.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.loadbalance; + +import io.grpc.Attributes; +import io.grpc.ConnectivityStateInfo; +import io.grpc.EquivalentAddressGroup; +import io.grpc.LoadBalancer; +import io.grpc.ConnectivityState; +import lombok.Data; + +/** + * The grpc SubChannels. + * + * @author zhanglei + */ +public class SubChannels { + + private static final Attributes.Key> STATE_INFO_KEY = Attributes.Key.create("state-info"); + + private static final Attributes.Key> WEIGHT_KEY = Attributes.Key.create("weight"); + + /** + * CreateSubChannel. + * + * @param helper helper + * @param addressGroup addressGroup + * @param attributes attributes + * @return LoadBalancer.Subchannel subchannel + */ + public static LoadBalancer.Subchannel createSubChannel(final LoadBalancer.Helper helper, + final EquivalentAddressGroup addressGroup, + final Attributes attributes) { + final Attributes newAttributes = attributes.toBuilder() + .set(STATE_INFO_KEY, new Ref<>(ConnectivityStateInfo.forNonError(ConnectivityState.IDLE))) + .build(); + return helper.createSubchannel(LoadBalancer.CreateSubchannelArgs + .newBuilder() + .setAddresses(addressGroup) + .setAttributes(newAttributes) + .build() + ); + } + + /** + * Create Attributes. + * + * @param weight weight + * @return Attributes attributes + */ + public static Attributes createAttributes(final int weight) { + return Attributes.newBuilder() + .set(WEIGHT_KEY, new Ref<>(weight)) + .build(); + } + + /** + * Get weight. weight + * + * @param subchannel subchannel + * @return int i + */ + public static int getWeight(final LoadBalancer.Subchannel subchannel) { + return getAttributeValue(subchannel, WEIGHT_KEY, 0); + } + + /** + * Get ConnectivityStateInfo. + * + * @param subchannel subchannel + * @return ConnectivityStateInfo info + */ + public static ConnectivityStateInfo getStateInfo(final LoadBalancer.Subchannel subchannel) { + return getAttributeValue(subchannel, STATE_INFO_KEY, null); + } + + /** + * SetStateInfo. + * + * @param subchannel subchannel + * @param value value + */ + public static void setStateInfo(final LoadBalancer.Subchannel subchannel, final ConnectivityStateInfo value) { + setAttributeValue(subchannel, STATE_INFO_KEY, value); + } + + private static T getAttributeValue(final LoadBalancer.Subchannel subchannel, final Attributes.Key> key, final T defaultValue) { + final Ref ref = subchannel.getAttributes().get(key); + return ref == null ? defaultValue : ref.value; + } + + private static void setAttributeValue(final LoadBalancer.Subchannel subchannel, final Attributes.Key> key, final T newValue) { + final Ref targetRef = subchannel.getAttributes().get(key); + if (targetRef != null) { + targetRef.value = newValue; + } + } + + /** + * Set AttributeValue. + * + * @param subchannel subchannel + * @param key key + * @param newAttributes newAttributes + * @param t + */ + private static void setAttributeValue(final LoadBalancer.Subchannel subchannel, final Attributes.Key> key, final Attributes newAttributes) { + final Ref newValueRef = newAttributes.get(key); + if (newValueRef != null) { + setAttributeValue(subchannel, key, newValueRef.value); + } + } + + /** + * UpdateAttributes. + * + * @param subchannel newAttributes + * @param attributes attributes + */ + public static void updateAttributes(final LoadBalancer.Subchannel subchannel, final Attributes attributes) { + setAttributeValue(subchannel, WEIGHT_KEY, attributes); + } + + @Data + static final class Ref { + + private T value; + + Ref(final T value) { + this.value = value; + } + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/SubchannelCopy.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/SubchannelCopy.java new file mode 100644 index 000000000000..3f178b27c337 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/SubchannelCopy.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.loadbalance; + +import io.grpc.ConnectivityStateInfo; +import io.grpc.EquivalentAddressGroup; +import io.grpc.LoadBalancer; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * SubchannelCopy. + * + * @author zhanglei + */ +@EqualsAndHashCode +@Getter +public class SubchannelCopy { + + private final int weight; + + private final LoadBalancer.Subchannel channel; + + private final EquivalentAddressGroup addressGroup; + + private final ConnectivityStateInfo state; + + public SubchannelCopy(final LoadBalancer.Subchannel channel) { + this.channel = channel; + this.addressGroup = channel.getAddresses(); + this.weight = SubChannels.getWeight(channel); + this.state = SubChannels.getStateInfo(channel); + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/AbstractPicker.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/AbstractPicker.java new file mode 100644 index 000000000000..96384e2e337c --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/AbstractPicker.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.loadbalance.picker; + +import io.grpc.LoadBalancer; + +/** + * Picker abstract. + * + * @author zhanglei + */ +public abstract class AbstractPicker extends LoadBalancer.SubchannelPicker { + + /** + * The target picker is equivalent to this. + * + * @param picker target picker + * @return picker is equivalent + */ + public abstract boolean isEquivalentTo(AbstractPicker picker); + + /** + * Get the target subChannels. + * + * @return subChannels infos + */ + public abstract String getSubchannelsInfo(); +} + diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/AbstractReadyPicker.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/AbstractReadyPicker.java new file mode 100644 index 000000000000..c009946b5db4 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/AbstractReadyPicker.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.loadbalance.picker; + +import io.grpc.ConnectivityState; +import io.grpc.LoadBalancer; +import io.grpc.Status; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.dromara.soul.plugin.grpc.loadbalance.SubchannelCopy; + +import java.util.HashSet; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; + +/** + * The AbstractReadyPicker result. + * + * @author zhanglei + */ +@Slf4j +public abstract class AbstractReadyPicker extends AbstractPicker implements Picker { + + private final boolean hasIdleNode; + + private final List list; + + AbstractReadyPicker(final List list) { + this.list = list.stream().map(SubchannelCopy::new).collect(Collectors.toList()); + this.hasIdleNode = hasIdleNode(); + } + + private boolean hasIdleNode() { + return this.list.stream().anyMatch(r -> { + final ConnectivityState state = r.getState().getState(); + return state == ConnectivityState.IDLE || state == ConnectivityState.CONNECTING; + }); + } + + @Override + public LoadBalancer.PickResult pickSubchannel(final LoadBalancer.PickSubchannelArgs args) { + final List list = getSubchannels(); + if (CollectionUtils.isEmpty(list)) { + return getErrorPickResult(); + } + SubchannelCopy channel = pick(getSubchannels()); + return channel == null ? getErrorPickResult() : LoadBalancer.PickResult.withSubchannel(channel.getChannel()); + } + + /** + * Choose subChannel. + * + * @param list subChannel list + * @return result subChannel + */ + protected abstract SubchannelCopy pick(List list); + + @Override + public List getSubchannels() { + return list.stream().filter(r -> { + final ConnectivityState state = r.getState().getState(); + return state == ConnectivityState.READY; + }).collect(Collectors.toList()); + } + + private LoadBalancer.PickResult getErrorPickResult() { + if (hasIdleNode) { + return LoadBalancer.PickResult.withNoResult(); + } else { + return LoadBalancer.PickResult.withError(Status.UNAVAILABLE.withCause(new NoSuchElementException()).withDescription("can not find the subChannel") + ); + } + } + + @Override + public boolean isEquivalentTo(final AbstractPicker picker) { + if (!(picker instanceof AbstractReadyPicker)) { + return false; + } + AbstractReadyPicker other = (AbstractReadyPicker) picker; + // the lists cannot contain duplicate subchannels + return other == this || (list.size() == other.list.size() + && new HashSet<>(list).containsAll(other.list)); + } + + @Override + public String getSubchannelsInfo() { + final List infos = this.list.stream().map(r -> "Subchannel" + + "{ weight=" + r.getWeight() + + ", readyState=\"" + r.getState().toString() + "\"" + + ", address=\"" + r.getChannel().getAddresses() + "\"" + + "}") + .collect(Collectors.toList()); + return "[ " + String.join(",", infos) + " ]"; + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/EmptyPicker.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/EmptyPicker.java new file mode 100644 index 000000000000..23a803ca475d --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/EmptyPicker.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.loadbalance.picker; + +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import io.grpc.LoadBalancer; +import io.grpc.Status; + +/** + * The empty picker. + * + * @author zhanglei + */ +public class EmptyPicker extends AbstractPicker { + + private final Status status; + + public EmptyPicker(final Status status) { + this.status = Preconditions.checkNotNull(status, "status"); + } + + @Override + public LoadBalancer.PickResult pickSubchannel(final LoadBalancer.PickSubchannelArgs args) { + return status.isOk() ? LoadBalancer.PickResult.withNoResult() : LoadBalancer.PickResult.withError(status); + } + + @Override + public boolean isEquivalentTo(final AbstractPicker picker) { + return picker instanceof EmptyPicker && (Objects.equal(status, ((EmptyPicker) picker).status) || (status.isOk() && ((EmptyPicker) picker).status.isOk())); + } + + @Override + public String getSubchannelsInfo() { + return "[]"; + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/Picker.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/Picker.java new file mode 100644 index 000000000000..de3c417cd06b --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/Picker.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.loadbalance.picker; + +import org.dromara.soul.plugin.grpc.loadbalance.SubchannelCopy; + +import java.util.List; + +/** + * Picker. + * + * @author zhanglei + */ +public interface Picker { + + /** + * get channels. + * + * @return List list + */ + List getSubchannels(); +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/RandomPicker.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/RandomPicker.java new file mode 100644 index 000000000000..9487fbafec8c --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/RandomPicker.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.loadbalance.picker; + +import io.grpc.LoadBalancer; +import org.apache.commons.collections4.CollectionUtils; +import org.dromara.soul.plugin.grpc.loadbalance.SubchannelCopy; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * RandomPicker. + * + * @author zhanglei + */ +public class RandomPicker extends AbstractReadyPicker { + + public RandomPicker(final List list) { + super(list); + } + + @Override + protected SubchannelCopy pick(final List list) { + if (CollectionUtils.isEmpty(list)) { + return null; + } + if (list.size() == 1) { + return list.get(0); + } + int index = getRandomIndexByWeight(list); + return list.get(index); + } + + private int getRandomIndexByWeight(final List list) { + final int sumWeight = list.stream().mapToInt(SubchannelCopy::getWeight).sum(); + if (sumWeight <= 0) { + return ThreadLocalRandom.current().nextInt(list.size()); + } + int randomInt = ThreadLocalRandom.current().nextInt(sumWeight); + int sumI = 0; + for (int i = 0; i < list.size(); i++) { + sumI += list.get(i).getWeight(); + if (randomInt < sumI) { + return i; + } + } + return list.size() - 1; + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/RoundRobinPicker.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/RoundRobinPicker.java new file mode 100644 index 000000000000..afed7591026a --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/RoundRobinPicker.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.loadbalance.picker; + +import io.grpc.LoadBalancer; +import org.apache.commons.collections4.CollectionUtils; +import org.dromara.soul.plugin.grpc.loadbalance.SubchannelCopy; + +import java.util.List; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +/** + * RoundRobin picker. + * + * @author zhanglei + */ +public class RoundRobinPicker extends AbstractReadyPicker { + + private static final AtomicIntegerFieldUpdater INDEX_UPDATER = AtomicIntegerFieldUpdater.newUpdater(RoundRobinPicker.class, "index"); + + /** + * AtomicIntegerFieldUpdater index. + */ + @SuppressWarnings("unused") + private volatile Integer index = 0; + + public RoundRobinPicker(final List list) { + super(list); + } + + @Override + protected SubchannelCopy pick(final List list) { + if (CollectionUtils.isEmpty(list)) { + return null; + } + final int size = list.size(); + if (size == 1) { + return list.get(0); + } + int i = INDEX_UPDATER.incrementAndGet(this); + if (i >= size) { + int oldi = i; + i %= size; + INDEX_UPDATER.compareAndSet(this, oldi, i); + } + return list.get(i); + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/param/BodyParamPlugin.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/param/BodyParamPlugin.java new file mode 100644 index 000000000000..9ad74a44547b --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/param/BodyParamPlugin.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.param; + +import org.dromara.soul.common.constant.Constants; +import org.dromara.soul.common.enums.PluginEnum; +import org.dromara.soul.common.enums.RpcTypeEnum; +import org.dromara.soul.common.utils.HttpParamConverter; +import org.dromara.soul.plugin.api.SoulPlugin; +import org.dromara.soul.plugin.api.SoulPluginChain; +import org.dromara.soul.plugin.api.context.SoulContext; +import org.springframework.http.MediaType; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.web.reactive.function.server.HandlerStrategies; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Objects; + +/** + * The type Body param plugin. + * + * @author zhanglei + */ +public class BodyParamPlugin implements SoulPlugin { + + private final List> messageReaders; + + /** + * Instantiates a new Body param plugin. + */ + public BodyParamPlugin() { + this.messageReaders = HandlerStrategies.withDefaults().messageReaders(); + } + + @Override + public Mono execute(final ServerWebExchange exchange, final SoulPluginChain chain) { + final ServerHttpRequest request = exchange.getRequest(); + final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT); + if (Objects.nonNull(soulContext) && RpcTypeEnum.GRPC.getName().equals(soulContext.getRpcType())) { + MediaType mediaType = request.getHeaders().getContentType(); + ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders); + if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) { + return body(exchange, serverRequest, chain); + } + if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)) { + return formData(exchange, serverRequest, chain); + } + return query(exchange, serverRequest, chain); + } + return chain.execute(exchange); + } + + @Override + public int getOrder() { + return PluginEnum.GRPC.getCode() - 1; + } + + @Override + public String named() { + return PluginEnum.GRPC.getName(); + } + + private Mono body(final ServerWebExchange exchange, final ServerRequest serverRequest, final SoulPluginChain chain) { + return serverRequest.bodyToMono(String.class) + .switchIfEmpty(Mono.defer(() -> Mono.just(""))) + .flatMap(body -> { + exchange.getAttributes().put(Constants.GRPC_PARAMS, body); + return chain.execute(exchange); + }); + } + + private Mono formData(final ServerWebExchange exchange, final ServerRequest serverRequest, final SoulPluginChain chain) { + return serverRequest.formData() + .switchIfEmpty(Mono.defer(() -> Mono.just(new LinkedMultiValueMap<>()))) + .flatMap(map -> { + exchange.getAttributes().put(Constants.GRPC_PARAMS, HttpParamConverter.toMap(() -> map)); + return chain.execute(exchange); + }); + } + + private Mono query(final ServerWebExchange exchange, final ServerRequest serverRequest, final SoulPluginChain chain) { + exchange.getAttributes().put(Constants.GRPC_PARAMS, + HttpParamConverter.ofString(() -> serverRequest.uri().getQuery())); + return chain.execute(exchange); + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/CompleteObserver.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/CompleteObserver.java new file mode 100644 index 000000000000..764ce3effcc7 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/CompleteObserver.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.proto; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import io.grpc.stub.StreamObserver; + +/** + * A holding a future which completes when the rpc terminates. + * + * @author zhanglei + */ +public class CompleteObserver implements StreamObserver { + + private final SettableFuture doneFuture; + + public CompleteObserver() { + this.doneFuture = SettableFuture.create(); + } + + @Override + public synchronized void onCompleted() { + doneFuture.set(null); + } + + @Override + public synchronized void onError(final Throwable t) { + doneFuture.setException(t); + } + + @Override + public void onNext(final T next) { + } + + /** + * Returns a future which completes when the rpc finishes. The returned future fails if the rpc fails. + * + * @return ListenableFuture future + */ + public ListenableFuture getCompletionFuture() { + return doneFuture; + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/CompositeStreamObserver.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/CompositeStreamObserver.java new file mode 100644 index 000000000000..5b3c900e0bbd --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/CompositeStreamObserver.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.proto; + +import com.google.common.collect.ImmutableList; +import io.grpc.stub.StreamObserver; +import lombok.extern.slf4j.Slf4j; + +/** + * A which groups multiple observers and executes them all. + * + * @author zhanglei + */ +@Slf4j +public final class CompositeStreamObserver implements StreamObserver { + + private final ImmutableList> observers; + + private CompositeStreamObserver(final ImmutableList> observers) { + this.observers = observers; + } + + /** + * CompositeStreamObserver of. + * + * @param observers observers + * @param completeObserver completeObserver + * @param t + * @return CompositeStreamObserver compositeStreamObserver + */ + public static CompositeStreamObserver of(final StreamObserver observers, + final CompleteObserver completeObserver) { + return new CompositeStreamObserver<>(ImmutableList.of(observers, completeObserver)); + } + + @Override + public void onCompleted() { + for (StreamObserver observer : observers) { + try { + observer.onCompleted(); + } catch (Exception t) { + log.error("Exception in composite onComplete, moving on", t); + } + } + } + + @Override + public void onError(final Throwable t) { + for (StreamObserver observer : observers) { + try { + observer.onError(t); + } catch (Exception exception) { + log.error("Exception in composite onError, moving on", exception); + } + } + } + + @Override + public void onNext(final T value) { + for (StreamObserver observer : observers) { + try { + observer.onNext(value); + } catch (Exception exception) { + log.error("Exception in composite onNext, moving on", exception); + } + } + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/DynamicMessageMarshaller.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/DynamicMessageMarshaller.java new file mode 100644 index 000000000000..a2258760fc61 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/DynamicMessageMarshaller.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.proto; + +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.ExtensionRegistryLite; +import io.grpc.MethodDescriptor.Marshaller; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Dynamic messages. + * + * @author zhanglei + */ +public class DynamicMessageMarshaller implements Marshaller { + + private final Descriptor messageDescriptor; + + public DynamicMessageMarshaller(final Descriptor messageDescriptor) { + this.messageDescriptor = messageDescriptor; + } + + @Override + public DynamicMessage parse(final InputStream inputStream) { + try { + return DynamicMessage.newBuilder(messageDescriptor) + .mergeFrom(inputStream, ExtensionRegistryLite.getEmptyRegistry()) + .build(); + } catch (IOException e) { + throw new RuntimeException("Unable to merge from the supplied input stream", e); + } + } + + @Override + public InputStream stream(final DynamicMessage abstractMessage) { + return abstractMessage.toByteString().newInput(); + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/MessageWriter.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/MessageWriter.java new file mode 100644 index 000000000000..5ffa76c7eb43 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/MessageWriter.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.proto; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import com.google.protobuf.util.JsonFormat; +import io.grpc.stub.StreamObserver; +import lombok.extern.slf4j.Slf4j; + +/** + * MessageWriter. + * + * @author zhanglei + */ +@Slf4j +public final class MessageWriter implements StreamObserver { + + private final JsonFormat.Printer printer; + + private final SoulGrpcResponse results; + + private MessageWriter(final JsonFormat.Printer printer, final SoulGrpcResponse results) { + this.printer = printer; + this.results = results; + } + + /** + * New instance. + * + * @param registry registry + * @param results results + * @param t + * @return message message + */ + public static MessageWriter newInstance(final JsonFormat.TypeRegistry registry, final SoulGrpcResponse results) { + return new MessageWriter<>(JsonFormat.printer().usingTypeRegistry(registry), results); + } + + @Override + public void onNext(final T value) { + try { + results.setResult(printer.print(value)); + } catch (InvalidProtocolBufferException e) { + log.error("Skipping invalid response message", e); + } + } + + @Override + public void onError(final Throwable t) { + log.error("Messages write occur errors", t); + } + + @Override + public void onCompleted() { + log.info("Messages write complete"); + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcCallRequest.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcCallRequest.java new file mode 100644 index 000000000000..f8fb63ad3a8e --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcCallRequest.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.proto; + +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.stub.StreamObserver; +import lombok.Builder; +import lombok.Getter; + +/** + * SoulGrpcCallRequest. + * + * @author zhanglei + */ +@Builder +@Getter +public class SoulGrpcCallRequest { + + private Channel channel; + + private CallOptions callOptions; + + private DynamicMessage requests; + + private Descriptors.MethodDescriptor methodDescriptor; + + private StreamObserver responseObserver; +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcRequest.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcRequest.java new file mode 100644 index 000000000000..35e724d4a34b --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcRequest.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.proto; + +import com.google.protobuf.Descriptors.MethodDescriptor; +import com.google.protobuf.DynamicMessage; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.stub.StreamObserver; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +/** + * SoulGrpcRequest. + * + * @author zhanglei + */ +@Builder +@Getter +public class SoulGrpcRequest { + + private MethodDescriptor methodDescriptor; + + private Channel channel; + + private CallOptions callOptions; + + private List requests; + + private StreamObserver responseObserver; +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcResponse.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcResponse.java new file mode 100644 index 000000000000..55d10d3c030d --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcResponse.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.proto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * SoulGrpcResponse. + * + * @author zhangjikai + */ +@Data +public class SoulGrpcResponse implements Serializable { + private String result; +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/reflection/LookupServiceHandler.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/reflection/LookupServiceHandler.java new file mode 100644 index 000000000000..2944bce91699 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/reflection/LookupServiceHandler.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.reflection; + +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import com.google.protobuf.ByteString; +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.InvalidProtocolBufferException; +import io.grpc.reflection.v1alpha.ServerReflectionRequest; +import io.grpc.reflection.v1alpha.ServerReflectionResponse; +import io.grpc.stub.StreamObserver; +import org.dromara.soul.common.exception.SoulException; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.Map; + +/** + * LookupServiceHandler. + * + * @author zhanglei + */ +public class LookupServiceHandler implements StreamObserver { + + private final String serviceName; + + private final Set requestedDescriptors; + + private final SettableFuture resultFuture; + + private final Map resolvedDescriptors; + + private StreamObserver requestStream; + + private int outstandingRequests; + + public LookupServiceHandler(final String serviceName) { + this.serviceName = serviceName; + this.resultFuture = SettableFuture.create(); + this.resolvedDescriptors = new HashMap<>(); + this.requestedDescriptors = new HashSet<>(); + this.outstandingRequests = 0; + } + + /** + * Start the handler. + * + * @param requestStream stream + * @return ListenableFuture future + */ + public ListenableFuture start(final StreamObserver requestStream) { + this.requestStream = requestStream; + requestStream.onNext(requestForSymbol(serviceName)); + ++outstandingRequests; + return resultFuture; + } + + @Override + public void onNext(final ServerReflectionResponse response) { + ServerReflectionResponse.MessageResponseCase responseCase = response.getMessageResponseCase(); + switch (responseCase) { + case FILE_DESCRIPTOR_RESPONSE: + ImmutableSet descriptors = + parseDescriptors(response.getFileDescriptorResponse().getFileDescriptorProtoList()); + descriptors.forEach(d -> resolvedDescriptors.put(d.getName(), d)); + descriptors.forEach(this::processDependencies); + break; + default: + break; + } + } + + @Override + public void onError(final Throwable t) { + resultFuture.setException(new RuntimeException("Reflection lookup rpc failed for: " + serviceName, t)); + } + + @Override + public void onCompleted() { + if (!resultFuture.isDone()) { + resultFuture.setException(new RuntimeException("Unexpected end of rpc")); + } + } + + private ImmutableSet parseDescriptors(final List descriptorBytes) { + ImmutableSet.Builder resultBuilder = ImmutableSet.builder(); + for (ByteString fileDescriptorBytes : descriptorBytes) { + try { + resultBuilder.add(DescriptorProtos.FileDescriptorProto.parseFrom(fileDescriptorBytes)); + } catch (InvalidProtocolBufferException e) { + throw new SoulException(e); + } + } + return resultBuilder.build(); + } + + private void processDependencies(final DescriptorProtos.FileDescriptorProto fileDescriptor) { + fileDescriptor.getDependencyList().forEach(dep -> { + if (!resolvedDescriptors.containsKey(dep) && !requestedDescriptors.contains(dep)) { + requestedDescriptors.add(dep); + ++outstandingRequests; + requestStream.onNext(requestForDescriptor(dep)); + } + }); + + --outstandingRequests; + if (outstandingRequests == 0) { + resultFuture.set(DescriptorProtos.FileDescriptorSet.newBuilder() + .addAllFile(resolvedDescriptors.values()) + .build()); + requestStream.onCompleted(); + } + } + + private static ServerReflectionRequest requestForDescriptor(final String name) { + return ServerReflectionRequest.newBuilder() + .setFileByFilename(name) + .build(); + } + + private static ServerReflectionRequest requestForSymbol(final String symbol) { + return ServerReflectionRequest.newBuilder() + .setFileContainingSymbol(symbol) + .build(); + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/reflection/SoulGrpcReflectionClient.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/reflection/SoulGrpcReflectionClient.java new file mode 100644 index 000000000000..4e13cf6a8c5b --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/reflection/SoulGrpcReflectionClient.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.reflection; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.grpc.Channel; +import io.grpc.MethodDescriptor; +import io.grpc.reflection.v1alpha.ServerReflectionGrpc; +import io.grpc.reflection.v1alpha.ServerReflectionRequest; +import io.grpc.stub.StreamObserver; +import lombok.extern.slf4j.Slf4j; +import org.dromara.soul.common.exception.SoulException; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static io.grpc.MethodDescriptor.generateFullMethodName; + +/** + * The Soul Grpc Reflection Client. + * + * @author zhanglei + */ +@Slf4j +public final class SoulGrpcReflectionClient { + + private final Channel channel; + + /** + * key : package + service name. + * value : proro file. + */ + private final Map fileDescriptorCache = new ConcurrentHashMap<>(); + + private SoulGrpcReflectionClient(final Channel channel) { + this.channel = channel; + } + + /** + * A new reflection client using the supplied channel. + * + * @param channel channel + * @return SoulGrpcReflectionClient client + */ + public static SoulGrpcReflectionClient create(final Channel channel) { + return new SoulGrpcReflectionClient(channel); + } + + /** + * Get Grpc service by the remote server. + * + * @param serviceName serviceName + * @return ListenableFuture future + */ + public ListenableFuture lookupService(final String serviceName) { + LookupServiceHandler rpcHandler = new LookupServiceHandler(serviceName); + StreamObserver requestStream = ServerReflectionGrpc.newStub(channel) + .withDeadlineAfter(10000, TimeUnit.MILLISECONDS) + .serverReflectionInfo(rpcHandler); + return rpcHandler.start(requestStream); + } + + /** + * Resolve services. + * + * @param serviceName serviceName + * @return FileDescriptorSet fleDescriptorSet + */ + public DescriptorProtos.FileDescriptorSet resolveService(final String serviceName) { + return fileDescriptorCache.computeIfAbsent(serviceName, k -> { + try { + return this.lookupService(k).get(); + } catch (InterruptedException e) { + log.error("Resolve services get error", e); + throw new SoulException(e); + } catch (ExecutionException e) { + log.error("Resolve services get error", e); + throw new SoulException(e); + } + }); + } + + /** + * Fetch full method name. + * + * @param methodDescriptor methodDescriptor + * @return String str + */ + public String fetchFullMethodName(final Descriptors.MethodDescriptor methodDescriptor) { + String serviceName = methodDescriptor.getService().getFullName(); + String methodName = methodDescriptor.getName(); + return generateFullMethodName(serviceName, methodName); + } + + /** + * Fetch method type. + * + * @param methodDescriptor methodDescriptor + * @return MethodType + */ + public MethodDescriptor.MethodType fetchMethodType(final Descriptors.MethodDescriptor methodDescriptor) { + boolean clientStreaming = methodDescriptor.toProto().getClientStreaming(); + boolean serverStreaming = methodDescriptor.toProto().getServerStreaming(); + if (clientStreaming && serverStreaming) { + return MethodDescriptor.MethodType.BIDI_STREAMING; + } else if (!clientStreaming && !serverStreaming) { + return MethodDescriptor.MethodType.UNARY; + } else if (!clientStreaming) { + return MethodDescriptor.MethodType.SERVER_STREAMING; + } else { + return MethodDescriptor.MethodType.SERVER_STREAMING; + } + } + + /** + * Parse message. + * + * @param registry registry + * @param descriptor descriptor + * @param jsons jsons + * @return DynamicMessage + */ + public DynamicMessage parseToMessages(final JsonFormat.TypeRegistry registry, final Descriptors.Descriptor descriptor, final String jsons) { + JsonFormat.Parser parser = JsonFormat.parser().usingTypeRegistry(registry); + try { + DynamicMessage.Builder messageBuilder = DynamicMessage.newBuilder(descriptor); + parser.merge(jsons, messageBuilder); + return messageBuilder.build(); + } catch (InvalidProtocolBufferException e) { + throw new IllegalArgumentException("Unable to parse json text", e); + } + } + + /** + * Get FileDescriptor cache. + * + * @return map + */ + public Map getFileDescriptorCache() { + return fileDescriptorCache; + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/ServiceResolver.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/ServiceResolver.java new file mode 100644 index 000000000000..fe51a1ff0b0c --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/ServiceResolver.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.resolver; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.DescriptorProtos.FileDescriptorProto; +import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +import com.google.protobuf.Descriptors; +import com.google.protobuf.Descriptors.Descriptor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.soul.common.dto.MetaData; + +import java.util.HashMap; +import java.util.Map; + +/** + * Read proto file descriptors and extract method definitions. + * + * @author zhanglei + */ +@Slf4j +public final class ServiceResolver { + + private final ImmutableList fileDescriptors; + + private ServiceResolver(final Iterable fileDescriptors) { + this.fileDescriptors = ImmutableList.copyOf(fileDescriptors); + } + + /** + * Creates a resolver. + * + * @param descriptorSet descriptorSet + * @return ServiceResolver serviceResolver + */ + public static ServiceResolver fromFileDescriptorSet(final FileDescriptorSet descriptorSet) { + ImmutableMap descriptorProtoIndex = + computeDescriptorProtoIndex(descriptorSet); + Map descriptorCache = new HashMap<>(); + + ImmutableList.Builder result = ImmutableList.builder(); + for (FileDescriptorProto descriptorProto : descriptorSet.getFileList()) { + try { + result.add(descriptorFromProto(descriptorProto, descriptorProtoIndex, descriptorCache)); + } catch (Descriptors.DescriptorValidationException e) { + log.warn("Skipped descriptor " + descriptorProto.getName() + " due to error", e); + } + } + return new ServiceResolver(result.build()); + } + + /** + * Lists all the known message types. + * + * @return ImmutableSet set + */ + public ImmutableSet listMessageTypes() { + ImmutableSet.Builder resultBuilder = ImmutableSet.builder(); + fileDescriptors.forEach(d -> resultBuilder.addAll(d.getMessageTypes())); + return resultBuilder.build(); + } + + /** + * Resolve service method. + * + * @param metaData metaData + * @return MethodDescriptor + */ + public Descriptors.MethodDescriptor resolveServiceMethod(final MetaData metaData) { + String fullServiceName = metaData.getServiceName(); + String serviceName = fullServiceName.substring(fullServiceName.lastIndexOf(".") + 1); + String packageName = fullServiceName.substring(0, fullServiceName.lastIndexOf(".")); + Descriptors.ServiceDescriptor service = findService(packageName, serviceName); + Descriptors.MethodDescriptor method = service.findMethodByName(metaData.getMethodName()); + if (method == null) { + throw new IllegalArgumentException( + "Unable to find method " + packageName + + " in service " + serviceName); + } + return method; + } + + private Descriptors.ServiceDescriptor findService(final String packageName, final String serviceName) { + for (Descriptors.FileDescriptor fileDescriptor : fileDescriptors) { + if (!fileDescriptor.getPackage().equals(packageName)) { + continue; + } + + Descriptors.ServiceDescriptor serviceDescriptor = fileDescriptor.findServiceByName(serviceName); + if (serviceDescriptor != null) { + return serviceDescriptor; + } + } + throw new IllegalArgumentException("Unable to find service with name: " + serviceName); + } + + private static ImmutableMap computeDescriptorProtoIndex(final FileDescriptorSet fileDescriptorSet) { + ImmutableMap.Builder resultBuilder = ImmutableMap.builder(); + for (FileDescriptorProto descriptorProto : fileDescriptorSet.getFileList()) { + resultBuilder.put(descriptorProto.getName(), descriptorProto); + } + return resultBuilder.build(); + } + + private static Descriptors.FileDescriptor descriptorFromProto( + final FileDescriptorProto descriptorProto, + final ImmutableMap descriptorProtoIndex, + final Map descriptorCache) throws Descriptors.DescriptorValidationException { + // First check the cache. + String descriptorName = descriptorProto.getName(); + if (descriptorCache.containsKey(descriptorName)) { + return descriptorCache.get(descriptorName); + } + // Then fetch all the required dependencies recursively. + ImmutableList.Builder dependencies = ImmutableList.builder(); + for (String dependencyName : descriptorProto.getDependencyList()) { + if (!descriptorProtoIndex.containsKey(dependencyName)) { + throw new IllegalArgumentException("Could not find dependency: " + dependencyName); + } + FileDescriptorProto dependencyProto = descriptorProtoIndex.get(dependencyName); + dependencies.add(descriptorFromProto(dependencyProto, descriptorProtoIndex, descriptorCache)); + } + // Finally construct the actual descriptor. + Descriptors.FileDescriptor[] empty = new Descriptors.FileDescriptor[0]; + return Descriptors.FileDescriptor.buildFrom(descriptorProto, dependencies.build().toArray(empty)); + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulNameResolver.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulNameResolver.java new file mode 100644 index 000000000000..983229966e90 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulNameResolver.java @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.resolver; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import io.grpc.Attributes; +import io.grpc.NameResolver; +import io.grpc.Status; +import io.grpc.EquivalentAddressGroup; +import io.grpc.SynchronizationContext; +import io.grpc.internal.SharedResourceHolder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.dromara.soul.plugin.grpc.cache.ApplicationConfigCache; +import org.dromara.soul.plugin.grpc.loadbalance.GrpcAttributeUtils; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * SoulNameResolver. + * + * @author zhanglei + */ +@Slf4j +public class SoulNameResolver extends NameResolver implements Consumer { + + private boolean resolving; + + private Listener2 listener; + + private Executor executor; + + private final String appName; + + private final Attributes attributes; + + private final SynchronizationContext syncContext; + + private final List keep = null; + + private List instanceList = Lists.newArrayList(); + + private final SharedResourceHolder.Resource executorResource; + + public SoulNameResolver(final String appName, final Args args, final SharedResourceHolder.Resource executorResource) { + this.appName = appName; + this.executor = args.getOffloadExecutor(); + this.executorResource = executorResource; + this.attributes = Attributes.newBuilder().set(GrpcAttributeUtils.appName(), appName).build(); + this.syncContext = Objects.requireNonNull(args.getSynchronizationContext(), "syncContext"); + } + + @Override + public void start(final Listener2 listener) { + Preconditions.checkState(this.listener == null, "already started"); + this.executor = SharedResourceHolder.get(this.executorResource); + this.listener = Preconditions.checkNotNull(listener, "listener"); + ApplicationConfigCache.getInstance().watch(appName, this); + resolve(); + } + + @Override + public void accept(final Object o) { + syncContext.execute(() -> { + if (this.listener != null) { + resolve(); + } + }); + } + + @Override + public void refresh() { + Preconditions.checkState(this.listener != null, "not started"); + resolve(); + } + + private void resolve() { + log.info("Scheduled resolve for {}", this.appName); + if (this.resolving) { + return; + } + this.resolving = true; + this.executor.execute(new Resolve(this.listener, this.instanceList)); + } + + @Override + public String getServiceAuthority() { + return appName; + } + + @Override + public void shutdown() { + this.listener = null; + if (this.executor != null) { + this.executor = SharedResourceHolder.release(this.executorResource, this.executor); + } + this.instanceList = Lists.newArrayList(); + } + + private final class Resolve implements Runnable { + + private final Listener2 savedListener; + + private final List savedInstanceList; + + Resolve(final Listener2 listener, final List instanceList) { + this.savedListener = Objects.requireNonNull(listener, "listener"); + this.savedInstanceList = Objects.requireNonNull(instanceList, "instanceList"); + } + + @Override + public void run() { + final AtomicReference> resultContainer = new AtomicReference<>(); + try { + resultContainer.set(resolveInternal()); + } catch (final Exception e) { + this.savedListener.onError(Status.UNAVAILABLE.withCause(e) + .withDescription("Failed to update server list for " + SoulNameResolver.this.appName)); + resultContainer.set(Lists.newArrayList()); + } finally { + SoulNameResolver.this.syncContext.execute(() -> { + SoulNameResolver.this.resolving = false; + final List newInstanceList = resultContainer.get(); + if (newInstanceList != keep && SoulNameResolver.this.listener != null) { + SoulNameResolver.this.instanceList = newInstanceList; + } + }); + } + } + + private List resolveInternal() { + final String name = SoulNameResolver.this.appName; + SoulServiceInstanceLists soulServiceInstanceLists = ApplicationConfigCache.getInstance().get(name); + List newInstanceList = soulServiceInstanceLists.getSoulServiceInstances(); + log.info("Got {} candidate servers for {}", newInstanceList.size(), name); + if (CollectionUtils.isEmpty(newInstanceList)) { + log.info("No servers found for {}", name); + this.savedListener.onError(Status.UNAVAILABLE.withDescription("No servers found for " + name)); + return Lists.newArrayList(); + } + if (!needsToUpdateConnections(newInstanceList)) { + log.info("Nothing has changed... skipping update for {}", name); + return keep; + } + log.info("Ready to update server list for {}", name); + final List targets = newInstanceList.stream() + .map(instance -> { + log.info("Found gRPC server {}:{} for {}", instance.getHost(), instance.getPort(), name); + return SoulResolverHelper.convertToEquivalentAddressGroup(instance); + }).collect(Collectors.toList()); + this.savedListener.onResult(ResolutionResult.newBuilder() + .setAddresses(targets) + .setAttributes(attributes) + .build()); + log.info("Done updating server list for {}", name); + return newInstanceList; + } + + private boolean needsToUpdateConnections(final List newInstanceList) { + if (this.savedInstanceList.size() != newInstanceList.size()) { + return true; + } + for (final SoulServiceInstance instance : this.savedInstanceList) { + final String host = instance.getHost(); + final int port = instance.getPort(); + boolean isSame = newInstanceList.stream().anyMatch(newInstance -> host.equals(newInstance.getHost()) + && port == newInstance.getPort() + && isMetadataEquals(instance.getMetadata(), newInstance.getMetadata())); + if (!isSame) { + return true; + } + } + return false; + } + + private boolean isMetadataEquals(final Map metadata, final Map newMetadata) { + final String[] keys = {"weight"}; + for (String key : keys) { + final String value = metadata.get(key); + final String newValue = newMetadata.get(key); + if (!Objects.equals(value, newValue)) { + return false; + } + } + return true; + } + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulNameResolverProvider.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulNameResolverProvider.java new file mode 100644 index 000000000000..5f76563e0449 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulNameResolverProvider.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.resolver; + +import io.grpc.NameResolver; +import io.grpc.NameResolverProvider; +import io.grpc.internal.GrpcUtil; +import org.dromara.soul.common.enums.PluginEnum; + +import java.net.URI; + +/** + * SoulNameResolverProvider. + * + * @author zhanglei + */ +public class SoulNameResolverProvider extends NameResolverProvider { + + @Override + public NameResolver newNameResolver(final URI targetUri, final NameResolver.Args args) { + return new SoulNameResolver(targetUri.getPath(), args, GrpcUtil.SHARED_CHANNEL_EXECUTOR); + } + + @Override + protected boolean isAvailable() { + return true; + } + + @Override + protected int priority() { + return 6; + } + + @Override + public String getDefaultScheme() { + return PluginEnum.GRPC.getName(); + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulResolverHelper.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulResolverHelper.java new file mode 100644 index 000000000000..a5067fba0f3c --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulResolverHelper.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.resolver; + +import io.grpc.Attributes; +import io.grpc.EquivalentAddressGroup; +import org.dromara.soul.plugin.grpc.loadbalance.SubChannels; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.Map; + +/** + * SoulResolverHelper. + * + * @author zhanglei + */ +public class SoulResolverHelper { + + /** + * ConvertToEquivalentAddressGroup. + * + * @param instance instance + * @return EquivalentAddressGroup + */ + public static EquivalentAddressGroup convertToEquivalentAddressGroup(final SoulServiceInstance instance) { + return new EquivalentAddressGroup( + new InetSocketAddress(instance.getHost(), instance.getPort()), + createAttributes(instance) + ); + } + + /** + * CreateAttributes. + * + * @param instance instance + * @return Attributes + */ + private static Attributes createAttributes(final SoulServiceInstance instance) { + Map metadata = instance.getMetadata(); + if (metadata == null) { + metadata = Collections.emptyMap(); + } + return SubChannels.createAttributes(instance.getWeight()); + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulServiceInstance.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulServiceInstance.java new file mode 100644 index 000000000000..808a2985d2ff --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulServiceInstance.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.resolver; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * Soul Service instance. + * + * @author zhanglei + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class SoulServiceInstance { + + private String host; + + private int port; + + private Map metadata; + + public SoulServiceInstance(final String host, final int port) { + this(host, port, new HashMap<>()); + } + + /** + * Get weight. + * + * @return int i + */ + public int getWeight() { + final String weightValue = metadata.get("weight"); + if (StringUtils.isEmpty(weightValue)) { + return 0; + } + return Integer.parseInt(weightValue); + } +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulServiceInstanceLists.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulServiceInstanceLists.java new file mode 100644 index 000000000000..c6985777aa04 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulServiceInstanceLists.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.resolver; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Soul service instance list. + * + * @author zhanglei + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class SoulServiceInstanceLists { + + private CopyOnWriteArrayList soulServiceInstances; + + private String appName; +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/response/GrpcResponsePlugin.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/response/GrpcResponsePlugin.java new file mode 100644 index 000000000000..5d71040e0208 --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/response/GrpcResponsePlugin.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.response; + +import org.dromara.soul.common.constant.Constants; +import org.dromara.soul.common.enums.PluginEnum; +import org.dromara.soul.common.enums.RpcTypeEnum; +import org.dromara.soul.common.utils.JsonUtils; +import org.dromara.soul.plugin.api.SoulPlugin; +import org.dromara.soul.plugin.api.SoulPluginChain; +import org.dromara.soul.plugin.api.context.SoulContext; +import org.dromara.soul.plugin.api.result.SoulResultEnum; +import org.dromara.soul.plugin.base.utils.SoulResultWrap; +import org.dromara.soul.plugin.base.utils.WebFluxResultUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.Objects; + +/** + * this is grpc response plugin. + * + * @author zhanglei + */ +public class GrpcResponsePlugin implements SoulPlugin { + + /** + * Process the Web request and (optionally) delegate to the next + * {@code WebFilter} through the given {@link SoulPluginChain}. + * + * @param exchange the current server exchange + * @param chain provides a way to delegate to the next filter + * @return {@code Mono} to indicate when request processing is complete + */ + @Override + public Mono execute(final ServerWebExchange exchange, final SoulPluginChain chain) { + return chain.execute(exchange).then(Mono.defer(() -> { + final Object result = exchange.getAttribute(Constants.GRPC_RPC_RESULT); + if (Objects.isNull(result)) { + Object error = SoulResultWrap.error(SoulResultEnum.SERVICE_RESULT_ERROR.getCode(), SoulResultEnum.SERVICE_RESULT_ERROR.getMsg(), null); + return WebFluxResultUtils.result(exchange, error); + } + Object success = SoulResultWrap.success(SoulResultEnum.SUCCESS.getCode(), SoulResultEnum.SUCCESS.getMsg(), JsonUtils.removeClass(result)); + return WebFluxResultUtils.result(exchange, success); + })); + } + + @Override + public Boolean skip(final ServerWebExchange exchange) { + final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT); + assert soulContext != null; + return !Objects.equals(soulContext.getRpcType(), RpcTypeEnum.GRPC.getName()); + } + + @Override + public int getOrder() { + return PluginEnum.RESPONSE.getCode(); + } + + /** + * acquire plugin name. + * + * @return plugin name. + */ + @Override + public String named() { + return PluginEnum.RESPONSE.getName(); + } + +} diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/subscriber/GrpcMetaDataSubscriber.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/subscriber/GrpcMetaDataSubscriber.java new file mode 100644 index 000000000000..921eead5ef6e --- /dev/null +++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/subscriber/GrpcMetaDataSubscriber.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.soul.plugin.grpc.subscriber; + +import com.google.common.collect.Maps; +import org.apache.commons.collections4.CollectionUtils; +import org.dromara.soul.common.dto.MetaData; +import org.dromara.soul.common.enums.RpcTypeEnum; +import org.dromara.soul.plugin.grpc.cache.ApplicationConfigCache; +import org.dromara.soul.plugin.grpc.cache.GrpcClientCache; +import org.dromara.soul.plugin.grpc.resolver.SoulServiceInstance; +import org.dromara.soul.sync.data.api.MetaDataSubscriber; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +/** + * The grpc metadata subscribe. + * + * @author zhanglei + */ +public class GrpcMetaDataSubscriber implements MetaDataSubscriber { + + private static final ConcurrentMap META_DATA = Maps.newConcurrentMap(); + + @Override + public void onSubscribe(final MetaData metaData) { + if (RpcTypeEnum.GRPC.getName().equals(metaData.getRpcType())) { + if (metaData.getContextPath() == null) { + return; + } + MetaData metaExist = META_DATA.get(metaData.getPath()); + List prxList = ApplicationConfigCache.getInstance() + .get(metaData.getContextPath()).getSoulServiceInstances(); + if (CollectionUtils.isEmpty(prxList)) { + GrpcClientCache.initGrpcClient(metaData.getContextPath()); + } + boolean exist = prxList.stream() + .filter(instance -> { + return isEqual(instance, metaData.getAppName()); + }).count() > 0; + if (!exist) { + ApplicationConfigCache.getInstance().initPrx(metaData); + } + if (Objects.isNull(metaExist)) { + META_DATA.put(metaData.getPath(), metaData); + } + } + } + + @Override + public void unSubscribe(final MetaData metaData) { + if (RpcTypeEnum.GRPC.getName().equals(metaData.getRpcType())) { + List prxList = ApplicationConfigCache.getInstance() + .get(metaData.getPath()).getSoulServiceInstances(); + List removePrxList = prxList.stream() + .filter(instance -> { + return isEqual(instance, metaData.getAppName()); + }) + .collect(Collectors.toList()); + prxList.removeAll(removePrxList); + if (CollectionUtils.isEmpty(prxList)) { + META_DATA.remove(metaData.getPath()); + ApplicationConfigCache.getInstance().invalidate(metaData); + } + } + } + + private Boolean isEqual(final SoulServiceInstance instance, final String appName) { + String url = String.join(":", instance.getHost(), String.valueOf(instance.getPort())); + return url.equals(appName); + } +} diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-client/pom.xml b/soul-spring-boot-starter/soul-spring-boot-starter-client/pom.xml index c55ffcb6a8b4..4fecc48249f1 100644 --- a/soul-spring-boot-starter/soul-spring-boot-starter-client/pom.xml +++ b/soul-spring-boot-starter/soul-spring-boot-starter-client/pom.xml @@ -35,7 +35,8 @@ soul-spring-boot-starter-client-apache-dubbo soul-spring-boot-starter-client-sofa soul-spring-boot-starter-client-tars + soul-spring-boot-starter-client-grpc - \ No newline at end of file + diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/pom.xml b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/pom.xml new file mode 100644 index 000000000000..3e2decd3a2fb --- /dev/null +++ b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/pom.xml @@ -0,0 +1,48 @@ + + + + + + soul-spring-boot-starter-client + org.dromara + 2.2.1 + + 4.0.0 + + soul-spring-boot-starter-client-grpc + + + + org.springframework.boot + spring-boot-starter + + + org.dromara + soul-client-grpc + ${project.version} + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/java/org/dromara/soul/springboot/starter/client/grpc/SoulGrpcClientConfiguration.java b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/java/org/dromara/soul/springboot/starter/client/grpc/SoulGrpcClientConfiguration.java new file mode 100644 index 000000000000..aedc8de9fe3e --- /dev/null +++ b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/java/org/dromara/soul/springboot/starter/client/grpc/SoulGrpcClientConfiguration.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.dromara.soul.springboot.starter.client.grpc; + +import org.dromara.soul.client.grpc.GrpcClientBeanPostProcessor; +import org.dromara.soul.client.grpc.common.config.GrpcConfig; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Grpc type client bean postprocessor. + * + * @author tydhot + */ +@Configuration +public class SoulGrpcClientConfiguration { + /** + * Grpc service bean post processor sofa service bean post processor. + * + * @param grpcConfig the sofa config + * @return the grpc service bean post processor + */ + @Bean + public GrpcClientBeanPostProcessor grpcServiceBeanPostProcessor(final GrpcConfig grpcConfig) { + return new GrpcClientBeanPostProcessor(grpcConfig); + } + + /** + * Grpc config sofa config. + * + * @return the grpc config + */ + @Bean + @ConfigurationProperties(prefix = "soul.grpc") + public GrpcConfig grpcConfig() { + return new GrpcConfig(); + } +} diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.factories b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..de3972df885e --- /dev/null +++ b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.dromara.soul.springboot.starter.client.grpc.SoulGrpcClientConfiguration diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.provides b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.provides new file mode 100644 index 000000000000..9cc6a041725a --- /dev/null +++ b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.provides @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +provides: soul-spring-boot-starter-client-grpc diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-plugin/pom.xml b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/pom.xml index 692a21f2278a..5fd0b4f0ecd7 100644 --- a/soul-spring-boot-starter/soul-spring-boot-starter-plugin/pom.xml +++ b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/pom.xml @@ -46,7 +46,8 @@ soul-spring-boot-starter-plugin-resilience4j soul-spring-boot-starter-plugin-tars soul-spring-boot-starter-plugin-context-path + soul-spring-boot-starter-plugin-grpc - \ No newline at end of file + diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/pom.xml b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/pom.xml new file mode 100644 index 000000000000..4cb893557d14 --- /dev/null +++ b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/pom.xml @@ -0,0 +1,41 @@ + + + + + soul-spring-boot-starter-plugin + org.dromara + 2.2.1 + + 4.0.0 + + soul-spring-boot-starter-plugin-grpc + + + + org.dromara + soul-plugin-grpc + ${project.version} + + + org.springframework.boot + spring-boot-starter + + + diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/java/org/dromara/soul/spring/boot/starter/plugin/grpc/GrpcPluginConfiguration.java b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/java/org/dromara/soul/spring/boot/starter/plugin/grpc/GrpcPluginConfiguration.java new file mode 100644 index 000000000000..7820303a8a9e --- /dev/null +++ b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/java/org/dromara/soul/spring/boot/starter/plugin/grpc/GrpcPluginConfiguration.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.dromara.soul.spring.boot.starter.plugin.grpc; + +import org.dromara.soul.plugin.api.SoulPlugin; +import org.dromara.soul.plugin.grpc.GrpcPlugin; +import org.dromara.soul.plugin.grpc.param.BodyParamPlugin; +import org.dromara.soul.plugin.grpc.response.GrpcResponsePlugin; +import org.dromara.soul.plugin.grpc.subscriber.GrpcMetaDataSubscriber; +import org.dromara.soul.sync.data.api.MetaDataSubscriber; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * The type grpc plugin configuration. + * + * @author zhanglei + */ +@Configuration +public class GrpcPluginConfiguration { + + /** + * grpc plugin soul plugin. + * + * @return the tars plugin + */ + @Bean + public SoulPlugin grpcPlugin() { + return new GrpcPlugin(); + } + + /** + * Body param plugin soul plugin. + * + * @return the soul plugin + */ + @Bean + public SoulPlugin bodyParamPlugin() { + return new BodyParamPlugin(); + } + + /** + * Grpc response plugin soul plugin. + * + * @return the soul plugin + */ + @Bean + public SoulPlugin grpcResponsePlugin() { + return new GrpcResponsePlugin(); + } + + /** + * Grpc meta data subscriber meta data subscriber. + * + * @return the meta data subscriber + */ + @Bean + public MetaDataSubscriber grpcMetaDataSubscriber() { + return new GrpcMetaDataSubscriber(); + } +} diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/resources/META-INF/spring.factories b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..4f03bb4f02a8 --- /dev/null +++ b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.dromara.soul.spring.boot.starter.plugin.grpc.GrpcPluginConfiguration diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/resources/META-INF/spring.provides b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/resources/META-INF/spring.provides new file mode 100644 index 000000000000..a1099f5407ed --- /dev/null +++ b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/resources/META-INF/spring.provides @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +provides: soul-spring-boot-starter-plugin-grpc