diff --git a/CHANGES.md b/CHANGES.md index 2fb88508c9e..e7041f0f223 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -33,6 +33,7 @@ Apollo 2.1.0 * [Move apollo-core, apollo-client, apollo-mockserver, apollo-openapi and apollo-client-config-data to apollo-java repo](https://github.com/apolloconfig/apollo/pull/4594) * [fix get the openapi interface that contains namespace information for deleted items](https://github.com/apolloconfig/apollo/pull/4596) * [A user-friendly config management page for apollo portal](https://github.com/apolloconfig/apollo/pull/4592) +* [feat: support use database as a registry](https://github.com/apolloconfig/apollo/pull/4595) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/11?closed=1) \ No newline at end of file diff --git a/apollo-adminservice/src/main/resources/application-database-discovery.properties b/apollo-adminservice/src/main/resources/application-database-discovery.properties new file mode 100644 index 00000000000..b67172a1fc9 --- /dev/null +++ b/apollo-adminservice/src/main/resources/application-database-discovery.properties @@ -0,0 +1,22 @@ +# +# Copyright 2022 Apollo Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +eureka.client.enabled=false +spring.cloud.discovery.enabled=false + +apollo.service.registry.enabled=true +apollo.service.registry.cluster=default +apollo.service.registry.heartbeatIntervalInSecond=10 + diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/JpaMapFieldJsonConverter.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/JpaMapFieldJsonConverter.java new file mode 100644 index 00000000000..c8f06699d14 --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/JpaMapFieldJsonConverter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.entity; + +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +@Converter(autoApply = true) +class JpaMapFieldJsonConverter implements AttributeConverter, String> { + + private static final Gson GSON = new Gson(); + + private static final TypeToken> TYPE_TOKEN = new TypeToken>() { + }; + + @SuppressWarnings("unchecked") + private static final Type TYPE = TYPE_TOKEN.getType(); + + @Override + public String convertToDatabaseColumn(Map attribute) { + return GSON.toJson(attribute); + } + + @Override + public Map convertToEntityAttribute(String dbData) { + return GSON.fromJson(dbData, TYPE); + } +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/ServiceRegistry.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/ServiceRegistry.java new file mode 100644 index 00000000000..9182ae38d1a --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/ServiceRegistry.java @@ -0,0 +1,152 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.entity; + +import com.ctrip.framework.apollo.biz.registry.ServiceInstance; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.PrePersist; +import javax.persistence.Table; + +/** + * use database as a registry instead of eureka, zookeeper, consul etc. + *

+ * persist {@link ServiceInstance} + */ +@Entity +@Table(name = "ServiceRegistry") +public class ServiceRegistry { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "Id") + private long id; + + @Column(name = "ServiceName", nullable = false) + private String serviceName; + + /** + * @see ServiceInstance#getUri() + */ + @Column(name = "Uri", nullable = false) + private String uri; + + /** + * @see ServiceInstance#getCluster() + */ + @Column(name = "Cluster", nullable = false) + private String cluster; + + @Column(name = "Metadata", nullable = false) + @Convert(converter = JpaMapFieldJsonConverter.class) + private Map metadata; + + @Column(name = "DataChange_CreatedTime", nullable = false) + private LocalDateTime dataChangeCreatedTime; + + /** + * modify by heartbeat + */ + @Column(name = "DataChange_LastTime", nullable = false) + private LocalDateTime dataChangeLastModifiedTime; + + @PrePersist + protected void prePersist() { + if (this.dataChangeCreatedTime == null) { + dataChangeCreatedTime = LocalDateTime.now(); + } + if (this.dataChangeLastModifiedTime == null) { + dataChangeLastModifiedTime = dataChangeCreatedTime; + } + } + + @Override + public String toString() { + return "Registry{" + + "id=" + id + + ", serviceName='" + serviceName + '\'' + + ", uri='" + uri + '\'' + + ", cluster='" + cluster + '\'' + + ", metadata='" + metadata + '\'' + + ", dataChangeCreatedTime=" + dataChangeCreatedTime + + ", dataChangeLastModifiedTime=" + dataChangeLastModifiedTime + + '}'; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getCluster() { + return cluster; + } + + public void setCluster(String cluster) { + this.cluster = cluster; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public LocalDateTime getDataChangeCreatedTime() { + return dataChangeCreatedTime; + } + + public void setDataChangeCreatedTime(LocalDateTime dataChangeCreatedTime) { + this.dataChangeCreatedTime = dataChangeCreatedTime; + } + + public LocalDateTime getDataChangeLastModifiedTime() { + return dataChangeLastModifiedTime; + } + + public void setDataChangeLastModifiedTime(LocalDateTime dataChangeLastModifiedTime) { + this.dataChangeLastModifiedTime = dataChangeLastModifiedTime; + } +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClient.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClient.java new file mode 100644 index 00000000000..15af064f09f --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClient.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry; + +import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryProperties; +import java.util.List; + +/** + * @see org.springframework.cloud.client.discovery.DiscoveryClient + */ +public interface DatabaseDiscoveryClient { + + /** + * find by {@link ApolloServiceRegistryProperties#getServiceName()}, + * then filter by {@link ApolloServiceRegistryProperties#getCluster()} + * + * @return empty list if there is no instance + */ + List getInstances(String serviceName); +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl.java new file mode 100644 index 00000000000..9907fc9be35 --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl.java @@ -0,0 +1,84 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * decorator pattern + *

+ * when database crash, even cannot register self instance to database, + *

+ * this decorator will ensure return's result contains self instance. + */ +public class DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl + implements DatabaseDiscoveryClient { + + private final DatabaseDiscoveryClient delegate; + + private final ServiceInstance selfInstance; + + public DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl( + DatabaseDiscoveryClient delegate, + ServiceInstance selfInstance + ) { + this.delegate = delegate; + this.selfInstance = selfInstance; + } + + static boolean containSelf(List serviceInstances, ServiceInstance selfInstance) { + final String selfServiceName = selfInstance.getServiceName(); + final URI selfUri = selfInstance.getUri(); + final String cluster = selfInstance.getCluster(); + for (ServiceInstance serviceInstance : serviceInstances) { + if (Objects.equals(selfServiceName, serviceInstance.getServiceName())) { + if (Objects.equals(selfUri, serviceInstance.getUri())) { + if (Objects.equals(cluster, serviceInstance.getCluster())) { + return true; + } + } + } + } + return false; + } + + /** + * if the serviceName is same with self, always return self's instance + * @return never be empty list when serviceName is same with self + */ + @Override + public List getInstances(String serviceName) { + if (Objects.equals(serviceName, this.selfInstance.getServiceName())) { + List serviceInstances = this.delegate.getInstances(serviceName); + if (containSelf(serviceInstances, this.selfInstance)) { + // contains self instance already + return serviceInstances; + } + + // add self instance to result + List result = new ArrayList<>(serviceInstances.size() + 1); + result.add(this.selfInstance); + result.addAll(serviceInstances); + return result; + } else { + return this.delegate.getInstances(serviceName); + } + } +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientImpl.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientImpl.java new file mode 100644 index 00000000000..f74b000a0cc --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientImpl.java @@ -0,0 +1,79 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry; + +import com.ctrip.framework.apollo.biz.entity.ServiceRegistry; +import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceDiscoveryProperties; +import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryProperties; +import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class DatabaseDiscoveryClientImpl implements DatabaseDiscoveryClient { + private final ServiceRegistryService serviceRegistryService; + + private final ApolloServiceDiscoveryProperties discoveryProperties; + + private final String cluster; + + public DatabaseDiscoveryClientImpl( + ServiceRegistryService serviceRegistryService, + ApolloServiceDiscoveryProperties discoveryProperties, + String cluster) { + this.serviceRegistryService = serviceRegistryService; + this.discoveryProperties = discoveryProperties; + this.cluster = cluster; + } + + /** + * find by {@link ApolloServiceRegistryProperties#getServiceName()} + */ + @Override + public List getInstances(String serviceName) { + final List serviceRegistryListFiltered; + { + LocalDateTime healthTime = LocalDateTime.now() + .minusSeconds(this.discoveryProperties.getHealthCheckIntervalInSecond()); + List filterByHealthCheck = + this.serviceRegistryService.findByServiceNameDataChangeLastModifiedTimeGreaterThan( + serviceName, healthTime + ); + serviceRegistryListFiltered = filterByCluster(filterByHealthCheck, this.cluster); + } + + return serviceRegistryListFiltered.stream() + .map(DatabaseDiscoveryClientImpl::convert) + .collect(Collectors.toList()); + } + + static ApolloServiceRegistryProperties convert(ServiceRegistry serviceRegistry) { + ApolloServiceRegistryProperties registration = new ApolloServiceRegistryProperties(); + registration.setServiceName(serviceRegistry.getServiceName()); + registration.setUri(serviceRegistry.getUri()); + registration.setCluster(serviceRegistry.getCluster()); + return registration; + } + + static List filterByCluster(List list, String cluster) { + return list.stream() + .filter(serviceRegistry -> Objects.equals(cluster, serviceRegistry.getCluster())) + .collect(Collectors.toList()); + } + +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientMemoryCacheDecoratorImpl.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientMemoryCacheDecoratorImpl.java new file mode 100644 index 00000000000..795e673caed --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientMemoryCacheDecoratorImpl.java @@ -0,0 +1,105 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry; + +import com.ctrip.framework.apollo.core.ServiceNameConsts; +import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * decorator pattern + *

+ * 1. use jvm memory as cache to decrease the read of database. + *

+ * 2. when database happened failure, return the cache in jvm memory. + */ +public class DatabaseDiscoveryClientMemoryCacheDecoratorImpl + implements DatabaseDiscoveryClient { + + private static final Logger log = LoggerFactory.getLogger( + DatabaseDiscoveryClientMemoryCacheDecoratorImpl.class + ); + + private final DatabaseDiscoveryClient delegate; + + private final Map> serviceName2ServiceInstances = new ConcurrentHashMap<>( + 8); + + private volatile ScheduledExecutorService scheduledExecutorService; + + private static final long SYNC_TASK_PERIOD_IN_SECOND = 5; + + public DatabaseDiscoveryClientMemoryCacheDecoratorImpl(DatabaseDiscoveryClient delegate) { + this.delegate = delegate; + } + + public void init() { + this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor( + ApolloThreadFactory + .create("DatabaseDiscoveryWithCache", true) + ); + scheduledExecutorService.scheduleAtFixedRate(this::updateCacheTask, + SYNC_TASK_PERIOD_IN_SECOND, SYNC_TASK_PERIOD_IN_SECOND, TimeUnit.SECONDS); + + // load them for init + try { + this.getInstances(ServiceNameConsts.APOLLO_CONFIGSERVICE); + } catch (Throwable t) { + log.error("fail to get instances of service name {}", ServiceNameConsts.APOLLO_CONFIGSERVICE, t); + } + try { + this.getInstances(ServiceNameConsts.APOLLO_ADMINSERVICE); + } catch (Throwable t) { + log.error("fail to get instances of service name {}", ServiceNameConsts.APOLLO_ADMINSERVICE, t); + } + } + + void updateCacheTask() { + try { + // for each service name, update their service instances in memory + this.serviceName2ServiceInstances.replaceAll( + (serviceName, serviceInstances) -> this.delegate.getInstances(serviceName) + ); + } catch (Throwable t) { + log.error("fail to read service instances from database", t); + } + } + + List readFromDatabase(String serviceName) { + return this.delegate.getInstances(serviceName); + } + + /** + * never throw {@link Throwable}, read from memory cache + */ + @Override + public List getInstances(String serviceName) { + // put serviceName as key to map, + // then the task use it to read service instances from database + this.serviceName2ServiceInstances.computeIfAbsent(serviceName, this::readFromDatabase); + // get from cache + return this.serviceName2ServiceInstances.getOrDefault(serviceName, Collections.emptyList()); + } +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseServiceRegistry.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseServiceRegistry.java new file mode 100644 index 00000000000..02c1ef919ff --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseServiceRegistry.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry; + +/** + * @see org.springframework.cloud.client.serviceregistry.ServiceRegistry + */ +public interface DatabaseServiceRegistry { + + /** + * register an instance to database + */ + void register(ServiceInstance instance); + + /** + * remove an instance from database + */ + void deregister(ServiceInstance instance); +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseServiceRegistryImpl.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseServiceRegistryImpl.java new file mode 100644 index 00000000000..6ec0b6fa0e2 --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/DatabaseServiceRegistryImpl.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry; + +import com.ctrip.framework.apollo.biz.entity.ServiceRegistry; +import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; +import java.time.LocalDateTime; + +public class DatabaseServiceRegistryImpl implements DatabaseServiceRegistry { + + private final ServiceRegistryService serviceRegistryService; + + public DatabaseServiceRegistryImpl( + ServiceRegistryService serviceRegistryService) { + this.serviceRegistryService = serviceRegistryService; + } + + static ServiceRegistry convert(ServiceInstance instance) { + ServiceRegistry serviceRegistry = new ServiceRegistry(); + serviceRegistry.setServiceName(instance.getServiceName()); + serviceRegistry.setUri(instance.getUri().toString()); + serviceRegistry.setCluster(instance.getCluster()); + serviceRegistry.setMetadata(instance.getMetadata()); + return serviceRegistry; + } + + public void register(ServiceInstance instance) { + ServiceRegistry serviceRegistry = convert(instance); + this.serviceRegistryService.saveIfNotExistByServiceNameAndUri(serviceRegistry); + } + + public void deregister(ServiceInstance instance) { + ServiceRegistry serviceRegistry = convert(instance); + this.serviceRegistryService.delete(serviceRegistry); + } +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/ServiceInstance.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/ServiceInstance.java new file mode 100644 index 00000000000..b9e225f756a --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/ServiceInstance.java @@ -0,0 +1,58 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry; + +import java.net.URI; +import java.util.Map; + +/** + * @see org.springframework.cloud.client.ServiceInstance + */ +public interface ServiceInstance { + + /** + * @return The service ID as registered. + */ + String getServiceName(); + + /** + * get the uri of a service instance, for example: + *

+ * + * @return The service URI address. + */ + URI getUri(); + + /** + * Tag a service instance for service discovery. + *

+ * so use cluster for service discovery. + * + * @return The cluster of the service instance. + */ + String getCluster(); + + /** + * @return The key / value pair metadata associated with the service instance. + * @see org.springframework.cloud.client.ServiceInstance#getMetadata() + */ + Map getMetadata(); +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/ApolloServiceDiscoveryAutoConfiguration.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/ApolloServiceDiscoveryAutoConfiguration.java new file mode 100644 index 00000000000..7af64768e06 --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/ApolloServiceDiscoveryAutoConfiguration.java @@ -0,0 +1,82 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry.configuration; + +import com.ctrip.framework.apollo.biz.registry.DatabaseDiscoveryClient; +import com.ctrip.framework.apollo.biz.registry.DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl; +import com.ctrip.framework.apollo.biz.registry.DatabaseDiscoveryClientImpl; +import com.ctrip.framework.apollo.biz.registry.DatabaseDiscoveryClientMemoryCacheDecoratorImpl; +import com.ctrip.framework.apollo.biz.registry.ServiceInstance; +import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryClearApplicationRunner; +import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceDiscoveryProperties; +import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty( + prefix = ApolloServiceDiscoveryProperties.PREFIX, + value = "enabled" +) +@EnableConfigurationProperties({ + ApolloServiceDiscoveryProperties.class, +}) +public class ApolloServiceDiscoveryAutoConfiguration { + + + private static DatabaseDiscoveryClient wrapMemoryCache(DatabaseDiscoveryClient discoveryClient) { + DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator + = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(discoveryClient); + decorator.init(); + return decorator; + } + + private static DatabaseDiscoveryClient wrapAlwaysAddSelfInstance( + DatabaseDiscoveryClient discoveryClient, + ServiceInstance selfInstance + ) { + return new DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl( + discoveryClient, selfInstance + ); + } + + @Bean + @ConditionalOnMissingBean + public DatabaseDiscoveryClient databaseDiscoveryClient( + ApolloServiceDiscoveryProperties discoveryProperties, + ServiceInstance selfServiceInstance, + ServiceRegistryService serviceRegistryService + ) { + DatabaseDiscoveryClient discoveryClient = new DatabaseDiscoveryClientImpl( + serviceRegistryService, discoveryProperties, selfServiceInstance.getCluster() + ); + return wrapMemoryCache( + wrapAlwaysAddSelfInstance(discoveryClient, selfServiceInstance) + ); + } + + @Bean + @ConditionalOnMissingBean + public ApolloServiceRegistryClearApplicationRunner apolloServiceRegistryClearApplicationRunner( + ServiceRegistryService serviceRegistryService + ) { + return new ApolloServiceRegistryClearApplicationRunner(serviceRegistryService); + } +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/ApolloServiceRegistryAutoConfiguration.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/ApolloServiceRegistryAutoConfiguration.java new file mode 100644 index 00000000000..628c8237f52 --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/ApolloServiceRegistryAutoConfiguration.java @@ -0,0 +1,69 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry.configuration; + +import com.ctrip.framework.apollo.biz.registry.DatabaseServiceRegistry; +import com.ctrip.framework.apollo.biz.registry.DatabaseServiceRegistryImpl; +import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryDeregisterApplicationListener; +import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryHeartbeatApplicationRunner; +import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryProperties; +import com.ctrip.framework.apollo.biz.repository.ServiceRegistryRepository; +import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty(prefix = ApolloServiceRegistryProperties.PREFIX, value = "enabled") +@EnableConfigurationProperties(ApolloServiceRegistryProperties.class) +public class ApolloServiceRegistryAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public ServiceRegistryService registryService(ServiceRegistryRepository repository) { + return new ServiceRegistryService(repository); + } + + @Bean + @ConditionalOnMissingBean + public DatabaseServiceRegistry databaseServiceRegistry( + ServiceRegistryService serviceRegistryService + ) { + return new DatabaseServiceRegistryImpl(serviceRegistryService); + } + + @Bean + @ConditionalOnMissingBean + public ApolloServiceRegistryHeartbeatApplicationRunner apolloServiceRegistryHeartbeatApplicationRunner( + ApolloServiceRegistryProperties registration, + DatabaseServiceRegistry serviceRegistry + ) { + return new ApolloServiceRegistryHeartbeatApplicationRunner(registration, serviceRegistry); + } + + @Bean + @ConditionalOnMissingBean + public ApolloServiceRegistryDeregisterApplicationListener apolloServiceRegistryDeregisterApplicationListener( + ApolloServiceRegistryProperties registration, + DatabaseServiceRegistry serviceRegistry + ) { + return new ApolloServiceRegistryDeregisterApplicationListener(registration, serviceRegistry); + } + +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceDiscoveryProperties.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceDiscoveryProperties.java new file mode 100644 index 00000000000..fb39077a919 --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceDiscoveryProperties.java @@ -0,0 +1,61 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry.configuration.support; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @see org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties + * @see org.springframework.cloud.consul.ConsulProperties + * @see org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean + */ +@ConfigurationProperties(prefix = ApolloServiceDiscoveryProperties.PREFIX) +public class ApolloServiceDiscoveryProperties { + + public static final String PREFIX = "apollo.service.discovery"; + + /** + * enable discovery of registry or not + */ + private boolean enabled = false; + + /** + * health check interval. + *

+ * if current time - the last time of instance's heartbeat < healthCheckInterval, + *

+ * then this instance is healthy. + */ + private long healthCheckIntervalInSecond = 61; + + public long getHealthCheckIntervalInSecond() { + return healthCheckIntervalInSecond; + } + + public void setHealthCheckIntervalInSecond(long healthCheckIntervalInSecond) { + this.healthCheckIntervalInSecond = healthCheckIntervalInSecond; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryClearApplicationRunner.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryClearApplicationRunner.java new file mode 100644 index 00000000000..afa5cb99cbf --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryClearApplicationRunner.java @@ -0,0 +1,76 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry.configuration.support; + +import com.ctrip.framework.apollo.biz.entity.ServiceRegistry; +import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; +import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; + +/** + * clear the unhealthy instances. + */ +public class ApolloServiceRegistryClearApplicationRunner + implements ApplicationRunner { + + private static final Logger log = LoggerFactory.getLogger( + ApolloServiceRegistryClearApplicationRunner.class); + + /** + * for {@link #clearUnhealthyInstances()} + */ + private final ScheduledExecutorService instanceClearScheduledExecutorService; + + + private final ServiceRegistryService serviceRegistryService; + + public ApolloServiceRegistryClearApplicationRunner( + ServiceRegistryService serviceRegistryService) { + this.serviceRegistryService = serviceRegistryService; + this.instanceClearScheduledExecutorService = Executors.newSingleThreadScheduledExecutor( + ApolloThreadFactory.create("ApolloRegistryServerClearInstances", true) + ); + } + + /** + * clear instance + */ + void clearUnhealthyInstances() { + try { + List serviceRegistryListDeleted = + this.serviceRegistryService.deleteTimeBefore(Duration.ofDays(1)); + if (serviceRegistryListDeleted != null && !serviceRegistryListDeleted.isEmpty()) { + log.info("clear {} unhealthy instances by scheduled task", serviceRegistryListDeleted.size()); + } + } catch (Throwable t) { + log.error("fail to clear unhealthy instances by scheduled task", t); + } + } + + @Override + public void run(ApplicationArguments args) throws Exception { + this.instanceClearScheduledExecutorService.scheduleAtFixedRate(this::clearUnhealthyInstances, 0, 1, TimeUnit.DAYS); + } +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryDeregisterApplicationListener.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryDeregisterApplicationListener.java new file mode 100644 index 00000000000..f7c9a529b2f --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryDeregisterApplicationListener.java @@ -0,0 +1,67 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry.configuration.support; + +import com.ctrip.framework.apollo.biz.registry.DatabaseServiceRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextClosedEvent; + +/** + * remove self before shutdown + */ +public class ApolloServiceRegistryDeregisterApplicationListener + implements ApplicationListener { + + private static final Logger log = LoggerFactory + .getLogger(ApolloServiceRegistryDeregisterApplicationListener.class); + private final ApolloServiceRegistryProperties registration; + + private final DatabaseServiceRegistry serviceRegistry; + + public ApolloServiceRegistryDeregisterApplicationListener( + ApolloServiceRegistryProperties registration, DatabaseServiceRegistry serviceRegistry) { + this.registration = registration; + this.serviceRegistry = serviceRegistry; + } + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + this.deregister(); + } + + private void deregister() { + try { + this.serviceRegistry.deregister(this.registration); + log.info( + "deregister success, '{}' uri '{}', cluster '{}'", + this.registration.getServiceName(), + this.registration.getUri(), + this.registration.getCluster() + ); + } catch (Throwable t) { + log.error( + "deregister fail, '{}' uri '{}', cluster '{}'", + this.registration.getServiceName(), + this.registration.getUri(), + this.registration.getCluster(), + t + ); + } + } +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryHeartbeatApplicationRunner.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryHeartbeatApplicationRunner.java new file mode 100644 index 00000000000..0cb0eac94d3 --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryHeartbeatApplicationRunner.java @@ -0,0 +1,81 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry.configuration.support; + +import com.ctrip.framework.apollo.biz.registry.DatabaseServiceRegistry; +import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; + +/** + * send heartbeat on runtime. + */ +public class ApolloServiceRegistryHeartbeatApplicationRunner + implements ApplicationRunner { + + private static final Logger log = LoggerFactory + .getLogger(ApolloServiceRegistryHeartbeatApplicationRunner.class); + + private final ApolloServiceRegistryProperties registration; + + private final DatabaseServiceRegistry serviceRegistry; + + /** + * for {@link #heartbeat()} + */ + private final ScheduledExecutorService heartbeatScheduledExecutorService; + + public ApolloServiceRegistryHeartbeatApplicationRunner( + ApolloServiceRegistryProperties registration, + DatabaseServiceRegistry serviceRegistry + ) { + this.registration = registration; + this.serviceRegistry = serviceRegistry; + this.heartbeatScheduledExecutorService = Executors.newSingleThreadScheduledExecutor( + ApolloThreadFactory.create("ApolloServiceRegistryHeartBeat", true) + ); + } + + @Override + public void run(ApplicationArguments args) throws Exception { + // register + log.info( + "register to database. '{}': uri '{}', cluster '{}' ", + this.registration.getServiceName(), + this.registration.getUri(), + this.registration.getCluster() + ); + // heartbeat as same as register + this.heartbeatScheduledExecutorService + .scheduleAtFixedRate(this::heartbeat, 0, this.registration.getHeartbeatIntervalInSecond(), + TimeUnit.SECONDS); + } + + private void heartbeat() { + try { + this.serviceRegistry.register(this.registration); + } catch (Throwable t) { + log.error("fail to send heartbeat by scheduled task", t); + } + } + +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryProperties.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryProperties.java new file mode 100644 index 00000000000..8652795dd7e --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryProperties.java @@ -0,0 +1,146 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry.configuration.support; + +import com.ctrip.framework.apollo.biz.registry.ServiceInstance; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.core.env.PropertyResolver; + +/** + * config of register. + * + * @see com.ctrip.framework.apollo.core.dto.ServiceDTO + * @see org.springframework.cloud.netflix.eureka.EurekaClientConfigBean + * @see org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean + */ +@ConfigurationProperties(prefix = ApolloServiceRegistryProperties.PREFIX) +public class ApolloServiceRegistryProperties implements ServiceInstance { + + public static final String PREFIX = "apollo.service.registry"; + + /** + * register self to registry or not + */ + private boolean enabled; + + /** + * @see com.ctrip.framework.apollo.core.ServiceNameConsts#APOLLO_CONFIGSERVICE + * @see com.ctrip.framework.apollo.core.ServiceNameConsts#APOLLO_ADMINSERVICE + */ + private String serviceName; + + /** + * @see ServiceInstance#getUri() + */ + private URI uri; + + /** + * @see ServiceInstance#getCluster() + */ + private String cluster; + + private Map metadata = new HashMap<>(8); + + /** + * heartbeat to registry in second. + */ + private long heartbeatIntervalInSecond = 10; + + @Autowired + private PropertyResolver propertyResolver; + + @Autowired + private InetUtils inetUtils; + + /** + * if user doesn't config, then resolve them on the runtime. + */ + @PostConstruct + public void postConstruct() { + if (this.serviceName == null) { + this.serviceName = propertyResolver.getRequiredProperty("spring.application.name"); + } + + if (this.uri == null) { + String host = this.inetUtils.findFirstNonLoopbackHostInfo().getIpAddress(); + Integer port = propertyResolver.getRequiredProperty("server.port", Integer.class); + String uriString = "http://" + host + ":" + port + "/"; + this.uri = URI.create(uriString); + } + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getServiceName() { + return serviceName; + } + + @Override + public URI getUri() { + return this.uri; + } + + /** + * custom the uri + * @see ServiceInstance#getUri() + */ + public void setUri(String uri) { + this.uri = URI.create(uri); + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + @Override + public String getCluster() { + return cluster; + } + + public void setCluster(String cluster) { + this.cluster = cluster; + } + + @Override + public Map getMetadata() { + return this.metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public long getHeartbeatIntervalInSecond() { + return heartbeatIntervalInSecond; + } + + public void setHeartbeatIntervalInSecond(long heartbeatIntervalInSecond) { + this.heartbeatIntervalInSecond = heartbeatIntervalInSecond; + } +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/package-info.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/package-info.java new file mode 100644 index 00000000000..7d8257c5f4b --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/registry/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +/** + * Use database as a registry without spring cloud. + *

+ * Maybe drop spring cloud in the feature. + */ +package com.ctrip.framework.apollo.biz.registry; \ No newline at end of file diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ServiceRegistryRepository.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ServiceRegistryRepository.java new file mode 100644 index 00000000000..1cacee0fad1 --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/ServiceRegistryRepository.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.repository; + +import com.ctrip.framework.apollo.biz.entity.ServiceRegistry; +import java.time.LocalDateTime; +import java.util.List; +import org.springframework.data.repository.PagingAndSortingRepository; + +public interface ServiceRegistryRepository extends PagingAndSortingRepository { + + List findByServiceNameAndDataChangeLastModifiedTimeGreaterThan( + String serviceName, LocalDateTime localDateTime + ); + + ServiceRegistry findByServiceNameAndUri(String serviceName, String uri); + + List deleteByDataChangeLastModifiedTimeLessThan(LocalDateTime localDateTime); + + int deleteByServiceNameAndUri(String serviceName, String uri); +} diff --git a/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ServiceRegistryService.java b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ServiceRegistryService.java new file mode 100644 index 00000000000..e841db1eb1e --- /dev/null +++ b/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ServiceRegistryService.java @@ -0,0 +1,69 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.service; + +import com.ctrip.framework.apollo.biz.entity.ServiceRegistry; +import com.ctrip.framework.apollo.biz.repository.ServiceRegistryRepository; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; +import org.springframework.transaction.annotation.Transactional; + +public class ServiceRegistryService { + + private final ServiceRegistryRepository repository; + + public ServiceRegistryService(ServiceRegistryRepository repository) { + this.repository = repository; + } + + public ServiceRegistry saveIfNotExistByServiceNameAndUri(ServiceRegistry serviceRegistry) { + ServiceRegistry serviceRegistrySaved = this.repository.findByServiceNameAndUri(serviceRegistry.getServiceName(), serviceRegistry.getUri()); + final LocalDateTime now = LocalDateTime.now(); + if (null == serviceRegistrySaved) { + serviceRegistrySaved = serviceRegistry; + serviceRegistrySaved.setDataChangeCreatedTime(now); + serviceRegistrySaved.setDataChangeLastModifiedTime(now); + } else { + // update + serviceRegistrySaved.setCluster(serviceRegistry.getCluster()); + serviceRegistrySaved.setMetadata(serviceRegistry.getMetadata()); + serviceRegistrySaved.setDataChangeLastModifiedTime(now); + } + return this.repository.save(serviceRegistrySaved); + } + + @Transactional + public void delete(ServiceRegistry serviceRegistry) { + this.repository.deleteByServiceNameAndUri( + serviceRegistry.getServiceName(), serviceRegistry.getUri() + ); + } + + public List findByServiceNameDataChangeLastModifiedTimeGreaterThan( + String serviceName, + LocalDateTime localDateTime + ) { + return this.repository.findByServiceNameAndDataChangeLastModifiedTimeGreaterThan(serviceName, localDateTime); + } + + @Transactional + public List deleteTimeBefore(Duration duration) { + LocalDateTime time = LocalDateTime.now().minus(duration); + return this.repository.deleteByDataChangeLastModifiedTimeLessThan(time); + } +} diff --git a/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/entity/JpaMapFieldJsonConverterTest.java b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/entity/JpaMapFieldJsonConverterTest.java new file mode 100644 index 00000000000..80f7d58158d --- /dev/null +++ b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/entity/JpaMapFieldJsonConverterTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.entity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; + +class JpaMapFieldJsonConverterTest { + + private final JpaMapFieldJsonConverter converter = new JpaMapFieldJsonConverter(); + + static String readAllContentOf(String path) throws IOException { + ClassPathResource classPathResource = new ClassPathResource(path); + byte[] bytes = Files.readAllBytes(classPathResource.getFile().toPath()); + return new String(bytes, StandardCharsets.UTF_8); + } + + @Test + void convertToDatabaseColumn_null() { + assertEquals("null", this.converter.convertToDatabaseColumn(null)); + } + + @Test + void convertToDatabaseColumn_empty() { + assertEquals("{}", this.converter.convertToDatabaseColumn(new HashMap<>(4))); + } + + @Test + void convertToDatabaseColumn_oneElement() throws IOException { + Map map = new HashMap<>(8); + map.put("a", "1"); + + String expected = readAllContentOf("json/converter/element.1.json"); + assertEquals(expected, this.converter.convertToDatabaseColumn(map)); + } + + @Test + void convertToDatabaseColumn_twoElement() throws IOException { + Map map = new HashMap<>(8); + map.put("a", "1"); + map.put("disableCheck", "true"); + + String expected = readAllContentOf("json/converter/element.2.json"); + assertEquals(expected, this.converter.convertToDatabaseColumn(map)); + } + + @Test + void convertToEntityAttribute_null() { + assertNull(this.converter.convertToEntityAttribute(null)); + assertNull(this.converter.convertToEntityAttribute("null")); + } + + @Test + void convertToEntityAttribute_null_oneElement() throws IOException { + Map map = new HashMap<>(8); + map.put("a", "1"); + + String content = readAllContentOf("json/converter/element.1.json"); + assertEquals(map, this.converter.convertToEntityAttribute(content)); + } + + @Test + void convertToEntityAttribute_null_twoElement() throws IOException { + Map map = new HashMap<>(8); + map.put("a", "1"); + map.put("disableCheck", "true"); + + String content = readAllContentOf("json/converter/element.2.json"); + assertEquals(map, this.converter.convertToEntityAttribute(content)); + } +} \ No newline at end of file diff --git a/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImplTest.java b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImplTest.java new file mode 100644 index 00000000000..70d950e173f --- /dev/null +++ b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImplTest.java @@ -0,0 +1,143 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry; + +import static com.ctrip.framework.apollo.biz.registry.ServiceInstanceFactory.newServiceInstance; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImplTest { + + @Test + void getInstances_other_service_name() { + final String otherServiceName = "other-service"; + DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); + Mockito.when(client.getInstances(otherServiceName)) + .thenReturn( + Collections.singletonList( + newServiceInstance(otherServiceName, "http://10.240.34.56:8081/", "beijing") + ) + ); + + final String selfServiceName = "self-service"; + ServiceInstance selfInstance = newServiceInstance( + selfServiceName, "http://10.240.34.56:8081/", "beijing" + ); + + DatabaseDiscoveryClient decorator = new DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl( + client, selfInstance + ); + + List serviceInstances = decorator.getInstances(otherServiceName); + assertEquals(1, serviceInstances.size()); + ServiceInstance otherServiceNameInstance = serviceInstances.get(0); + assertEquals(otherServiceName, otherServiceNameInstance.getServiceName()); + + Mockito.verify(client, Mockito.times(1)) + .getInstances(Mockito.eq(otherServiceName)); + + Mockito.verify(client, Mockito.never()) + .getInstances(Mockito.eq(selfServiceName)); + } + + @Test + void getInstances_contain_self() { + final String otherServiceName = "other-service"; + DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); + Mockito.when(client.getInstances(otherServiceName)) + .thenReturn( + Collections.singletonList( + newServiceInstance(otherServiceName, "http://10.240.34.56:8081/", "beijing") + ) + ); + + final String selfServiceName = "self-service"; + ServiceInstance selfInstance = newServiceInstance( + selfServiceName, "http://10.240.34.56:8081/", "beijing" + ); + Mockito.when(client.getInstances(selfServiceName)) + .thenReturn( + Arrays.asList( + selfInstance, + // same service name but different service instance + newServiceInstance(selfServiceName, "http://10.240.34.56:8082/", "beijing"), + newServiceInstance(selfServiceName, "http://10.240.34.56:8083/", "beijing") + ) + ); + + DatabaseDiscoveryClient decorator = new DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl( + client, selfInstance + ); + + List serviceInstances = decorator.getInstances(selfServiceName); + assertEquals(3, serviceInstances.size()); + + Mockito.verify(client, Mockito.times(1)) + .getInstances(Mockito.eq(selfServiceName)); + + Mockito.verify(client, Mockito.never()) + .getInstances(Mockito.eq(otherServiceName)); + } + + /** + * will add self + */ + @Test + void getInstances_same_service_name_without_self() { + final String otherServiceName = "other-service"; + DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); + Mockito.when(client.getInstances(otherServiceName)) + .thenReturn( + Collections.singletonList( + newServiceInstance(otherServiceName, "http://10.240.34.56:8081/", "beijing") + ) + ); + + final String selfServiceName = "self-service"; + ServiceInstance selfInstance = newServiceInstance( + selfServiceName, "http://10.240.34.56:8081/", "beijing" + ); + Mockito.when(client.getInstances(selfServiceName)) + .thenReturn( + Arrays.asList( + // same service name but different service instance + newServiceInstance(selfServiceName, "http://10.240.34.56:8082/", "beijing"), + newServiceInstance(selfServiceName, "http://10.240.34.56:8083/", "beijing") + ) + ); + + DatabaseDiscoveryClient decorator = new DatabaseDiscoveryClientAlwaysAddSelfInstanceDecoratorImpl( + client, selfInstance + ); + + List serviceInstances = decorator.getInstances(selfServiceName); + // because mocked data don't contain self instance + // after add self instance, there are 3 instances now + assertEquals(3, serviceInstances.size()); + + Mockito.verify(client, Mockito.times(1)) + .getInstances(Mockito.eq(selfServiceName)); + + Mockito.verify(client, Mockito.never()) + .getInstances(Mockito.eq(otherServiceName)); + } +} \ No newline at end of file diff --git a/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientImplTest.java b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientImplTest.java new file mode 100644 index 00000000000..6bd1107e073 --- /dev/null +++ b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientImplTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; + +import com.ctrip.framework.apollo.biz.entity.ServiceRegistry; +import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceDiscoveryProperties; +import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class DatabaseDiscoveryClientImplTest { + + private static ServiceRegistry newServiceRegistry( + String serviceName, String uri, String cluster, LocalDateTime dataChangeLastModifiedTime + ) { + ServiceRegistry serviceRegistry = new ServiceRegistry(); + serviceRegistry.setServiceName(serviceName); + serviceRegistry.setUri(uri); + serviceRegistry.setCluster(cluster); + serviceRegistry.setMetadata(new HashMap<>()); + serviceRegistry.setDataChangeCreatedTime(LocalDateTime.now()); + serviceRegistry.setDataChangeLastModifiedTime(dataChangeLastModifiedTime); + return serviceRegistry; + } + + private static ServiceRegistry newServiceRegistry(String serviceName, String uri, + String cluster) { + return newServiceRegistry(serviceName, uri, cluster, LocalDateTime.now()); + } + + @Test + void getInstances_filterByCluster() { + final String serviceName = "a-service"; + ServiceRegistryService serviceRegistryService = Mockito.mock(ServiceRegistryService.class); + { + List serviceRegistryList = Arrays.asList( + newServiceRegistry(serviceName, "http://localhost:8081/", "1"), + newServiceRegistry("b-service", "http://localhost:8082/", "2"), + newServiceRegistry("c-service", "http://localhost:8082/", "3") + ); + Mockito.when( + serviceRegistryService.findByServiceNameDataChangeLastModifiedTimeGreaterThan( + eq(serviceName), + any(LocalDateTime.class))) + .thenReturn(serviceRegistryList); + } + + DatabaseDiscoveryClient discoveryClient = new DatabaseDiscoveryClientImpl( + serviceRegistryService, + new ApolloServiceDiscoveryProperties(), + "1" + ); + + List serviceInstances = discoveryClient.getInstances(serviceName); + assertEquals(1, serviceInstances.size()); + assertEquals(serviceName, serviceInstances.get(0).getServiceName()); + assertEquals("1", serviceInstances.get(0).getCluster()); + } + + @Test + void getInstances_filterByHealthCheck() { + final String serviceName = "a-service"; + ServiceRegistryService serviceRegistryService = Mockito.mock(ServiceRegistryService.class); + + ServiceRegistry healthy = newServiceRegistry(serviceName, "http://localhost:8081/", "1", + LocalDateTime.now()); + Mockito.when( + serviceRegistryService.findByServiceNameDataChangeLastModifiedTimeGreaterThan( + eq(serviceName), + any(LocalDateTime.class))) + .thenReturn(Collections.singletonList(healthy)); + + DatabaseDiscoveryClient discoveryClient = new DatabaseDiscoveryClientImpl( + serviceRegistryService, + new ApolloServiceDiscoveryProperties(), + "1" + ); + + List serviceInstances = discoveryClient.getInstances(serviceName); + assertEquals(1, serviceInstances.size()); + assertEquals(serviceName, serviceInstances.get(0).getServiceName()); + assertEquals("http://localhost:8081/", serviceInstances.get(0).getUri().toString()); + assertEquals("1", serviceInstances.get(0).getCluster()); + } +} \ No newline at end of file diff --git a/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientMemoryCacheDecoratorImplTest.java b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientMemoryCacheDecoratorImplTest.java new file mode 100644 index 00000000000..097f35dc493 --- /dev/null +++ b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientMemoryCacheDecoratorImplTest.java @@ -0,0 +1,210 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry; + +import static com.ctrip.framework.apollo.biz.registry.ServiceInstanceFactory.newServiceInstance; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class DatabaseDiscoveryClientMemoryCacheDecoratorImplTest { + + @Test + void init() { + DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); + DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator + = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client); + decorator.init(); + } + + @Test + void updateCacheTask_empty() { + DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); + DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator + = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client); + decorator.updateCacheTask(); + + Mockito.verify(client, Mockito.never()).getInstances(Mockito.any()); + } + + @Test + void updateCacheTask_exception() { + final String serviceName = "a-service"; + DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); + Mockito.when(client.getInstances(serviceName)) + .thenReturn( + Arrays.asList( + newServiceInstance(serviceName, "http://10.240.34.56:8080/", "beijing"), + newServiceInstance(serviceName, "http://10.240.34.56:8081/", "beijing"), + newServiceInstance(serviceName, "http://10.240.34.56:8082/", "beijing") + ) + ); + DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator + = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client); + List list = decorator.getInstances(serviceName); + assertEquals(3, list.size()); + + // if database error + Mockito.when(client.getInstances(serviceName)) + .thenThrow(OutOfMemoryError.class); + assertThrows(OutOfMemoryError.class, () -> decorator.readFromDatabase(serviceName)); + + // task won't be interrupted by Throwable + decorator.updateCacheTask(); + + Mockito.verify(client, Mockito.times(3)).getInstances(serviceName); + } + + @Test + void getInstances_from_cache() { + DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); + Mockito.when(client.getInstances("a-service")) + .thenReturn( + Arrays.asList( + newServiceInstance("a-service", "http://10.240.34.56:8080/", "beijing"), + newServiceInstance("a-service", "http://10.240.34.56:8081/", "beijing") + ) + ); + Mockito.when(client.getInstances("b-service")) + .thenReturn( + Arrays.asList( + newServiceInstance("b-service", "http://10.240.56.78:8080/", "shanghai"), + newServiceInstance("b-service", "http://10.240.56.78:8081/", "shanghai"), + newServiceInstance("b-service", "http://10.240.56.78:8082/", "shanghai") + ) + ); + + DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator + = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client); + assertEquals(2, decorator.getInstances("a-service").size()); + assertEquals(2, decorator.getInstances("a-service").size()); + assertEquals(3, decorator.getInstances("b-service").size()); + assertEquals(3, decorator.getInstances("b-service").size()); + + // only invoke 1 times because always read from cache + Mockito.verify(client, Mockito.times(1)).getInstances("a-service"); + Mockito.verify(client, Mockito.times(1)).getInstances("b-service"); + } + + @Test + void getInstances_from_cache_when_database_updated() { + DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); + Mockito.when(client.getInstances("a-service")) + .thenReturn( + Arrays.asList( + newServiceInstance("a-service", "http://10.240.34.56:8080/", "beijing"), + newServiceInstance("a-service", "http://10.240.34.56:8081/", "beijing") + ) + ); + Mockito.when(client.getInstances("b-service")) + .thenReturn( + Arrays.asList( + newServiceInstance("b-service", "http://10.240.56.78:8080/", "shanghai"), + newServiceInstance("b-service", "http://10.240.56.78:8081/", "shanghai"), + newServiceInstance("b-service", "http://10.240.56.78:8082/", "shanghai") + ) + ); + + DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator + = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client); + assertEquals(2, decorator.getInstances("a-service").size()); + assertEquals(2, decorator.getInstances("a-service").size()); + assertEquals(3, decorator.getInstances("b-service").size()); + assertEquals(3, decorator.getInstances("b-service").size()); + + // only invoke 1 times because always read from cache + Mockito.verify(client, Mockito.times(1)).getInstances("a-service"); + Mockito.verify(client, Mockito.times(1)).getInstances("b-service"); + + // instances in database are changed + Mockito.when(client.getInstances("b-service")) + .thenReturn( + Collections.singletonList( + newServiceInstance("b-service", "http://10.240.56.78:8080/", "shanghai") + ) + ); + + // read again + assertEquals(2, decorator.getInstances("a-service").size()); + // cache doesn't update yet, so we still get 3 instances + assertEquals(3, decorator.getInstances("b-service").size()); + + // only invoke 1 times because always read from cache + Mockito.verify(client, Mockito.times(1)).getInstances("a-service"); + Mockito.verify(client, Mockito.times(1)).getInstances("b-service"); + + decorator.updateCacheTask(); + + // read again + assertEquals(2, decorator.getInstances("a-service").size()); + // cache updated already, so we still get 1 instances + assertEquals(1, decorator.getInstances("b-service").size()); + + // invoke 2 times because always read from database again by task + Mockito.verify(client, Mockito.times(2)).getInstances("a-service"); + Mockito.verify(client, Mockito.times(2)).getInstances("b-service"); + } + + + @Test + void getInstances_from_cache_when_database_crash() { + DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class); + Mockito.when(client.getInstances("a-service")) + .thenReturn( + Arrays.asList( + newServiceInstance("a-service", "http://10.240.34.56:8080/", "beijing"), + newServiceInstance("a-service", "http://10.240.34.56:8081/", "beijing") + ) + ); + Mockito.when(client.getInstances("b-service")) + .thenReturn( + Arrays.asList( + newServiceInstance("b-service", "http://10.240.56.78:8080/", "shanghai"), + newServiceInstance("b-service", "http://10.240.56.78:8081/", "shanghai"), + newServiceInstance("b-service", "http://10.240.56.78:8082/", "shanghai") + ) + ); + + DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator + = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client); + assertEquals(2, decorator.getInstances("a-service").size()); + assertEquals(2, decorator.getInstances("a-service").size()); + assertEquals(3, decorator.getInstances("b-service").size()); + assertEquals(3, decorator.getInstances("b-service").size()); + + // only invoke 1 times because always read from cache + Mockito.verify(client, Mockito.times(1)).getInstances("a-service"); + Mockito.verify(client, Mockito.times(1)).getInstances("b-service"); + + // database crash + Mockito.when(client.getInstances(Mockito.any())) + .thenThrow(OutOfMemoryError.class); + assertThrows(OutOfMemoryError.class, () -> decorator.readFromDatabase("a-service")); + assertThrows(OutOfMemoryError.class, () -> decorator.readFromDatabase("b-service")); + + // read again + assertEquals(2, decorator.getInstances("a-service").size()); + assertEquals(3, decorator.getInstances("b-service").size()); + + Mockito.verify(client, Mockito.times(2)).getInstances("a-service"); + Mockito.verify(client, Mockito.times(2)).getInstances("b-service"); + } +} \ No newline at end of file diff --git a/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryIntegrationTest.java b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryIntegrationTest.java new file mode 100644 index 00000000000..9ac46ed6ad4 --- /dev/null +++ b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryIntegrationTest.java @@ -0,0 +1,166 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry; + +import static com.ctrip.framework.apollo.biz.registry.ServiceInstanceFactory.newServiceInstance; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; +import com.ctrip.framework.apollo.biz.registry.DatabaseDiscoveryWithoutDecoratorIntegrationTest.ApolloServiceDiscoveryWithoutDecoratorAutoConfiguration; +import com.ctrip.framework.apollo.biz.registry.configuration.ApolloServiceDiscoveryAutoConfiguration; +import com.ctrip.framework.apollo.biz.registry.configuration.ApolloServiceRegistryAutoConfiguration; +import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceDiscoveryProperties; +import com.ctrip.framework.apollo.biz.repository.ServiceRegistryRepository; +import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; +import java.util.List; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; + +/** + * test when {@link DatabaseDiscoveryClient} is warped by decorator. + */ +@TestPropertySource( + properties = { + "apollo.service.registry.enabled=true", + "apollo.service.registry.cluster=default", + "apollo.service.discovery.enabled=true", + "spring.application.name=for-test-service", + "server.port=10000", + } +) +@ContextConfiguration(classes = { + ApolloServiceRegistryAutoConfiguration.class, + ApolloServiceDiscoveryAutoConfiguration.class, +}) +@EnableJpaRepositories(basePackageClasses = ServiceRegistryRepository.class) +public class DatabaseDiscoveryIntegrationTest extends AbstractIntegrationTest { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private DatabaseServiceRegistry serviceRegistry; + + @Autowired + private DatabaseDiscoveryClient discoveryClient; + + /** + * discover one after register, and delete it + */ + @Test + public void registerThenDiscoveryThenDelete() { + // register it + String serviceName = "a-service"; + String uri = "http://192.168.1.20:8080/"; + String cluster = "default"; + ServiceInstance instance = newServiceInstance( + serviceName, uri, cluster + ); + this.serviceRegistry.register(instance); + + // find it + List serviceInstances = this.discoveryClient.getInstances(serviceName); + assertEquals(1, serviceInstances.size()); + ServiceInstance actual = serviceInstances.get(0); + assertEquals(serviceName, actual.getServiceName()); + assertEquals(uri, actual.getUri().toString()); + assertEquals(cluster, actual.getCluster()); + assertEquals(0, actual.getMetadata().size()); + + // delete it + this.serviceRegistry.deregister(instance); + // because it save in memory, so we can still find it + assertEquals(1, this.discoveryClient.getInstances(serviceName).size()); + } + + /** + * diff cluster so cannot be discover + */ + @Test + public void registerThenDiscoveryNone() { + // register it + String serviceName = "b-service"; + ServiceInstance instance = newServiceInstance( + serviceName, "http://192.168.1.20:8080/", "cannot-be-discovery" + ); + this.serviceRegistry.register(instance); + + // find none + List serviceInstances = this.discoveryClient.getInstances(serviceName); + assertEquals(0, serviceInstances.size()); + } + + @Test + public void registerTwice() { + String serviceName = "c-service"; + ServiceInstance instance = newServiceInstance( + serviceName, "http://192.168.1.20:8080/", "default" + ); + + // register it + this.serviceRegistry.register(instance); + // register again + this.serviceRegistry.register(instance); + + // only discover one + List serviceInstances = this.discoveryClient.getInstances(serviceName); + assertEquals(1, serviceInstances.size()); + } + + @Test + public void registerTwoInstancesThenDeleteOne() { + final String serviceName = "d-service"; + final String cluster = "default"; + + this.serviceRegistry.register( + newServiceInstance( + serviceName, "http://192.168.1.20:8080/", cluster + ) + ); + this.serviceRegistry.register( + newServiceInstance( + serviceName, "http://192.168.1.20:10000/", cluster + ) + ); + + final List serviceInstances = this.discoveryClient.getInstances(serviceName); + assertEquals(2, serviceInstances.size()); + + for (ServiceInstance serviceInstance : serviceInstances) { + assertEquals(serviceName, serviceInstance.getServiceName()); + assertEquals(cluster, serviceInstance.getCluster()); + assertEquals(0, serviceInstance.getMetadata().size()); + } + + // delete one + this.serviceRegistry.deregister( + newServiceInstance( + serviceName, "http://192.168.1.20:10000/", cluster + ) + ); + + // because it save in memory, so we can still find it + assertEquals(2, this.discoveryClient.getInstances(serviceName).size()); + } +} diff --git a/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryWithoutDecoratorIntegrationTest.java b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryWithoutDecoratorIntegrationTest.java new file mode 100644 index 00000000000..dffe06f0980 --- /dev/null +++ b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryWithoutDecoratorIntegrationTest.java @@ -0,0 +1,198 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry; + +import static com.ctrip.framework.apollo.biz.registry.ServiceInstanceFactory.newServiceInstance; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; +import com.ctrip.framework.apollo.biz.registry.DatabaseDiscoveryWithoutDecoratorIntegrationTest.ApolloServiceDiscoveryWithoutDecoratorAutoConfiguration; +import com.ctrip.framework.apollo.biz.registry.configuration.ApolloServiceDiscoveryAutoConfiguration; +import com.ctrip.framework.apollo.biz.registry.configuration.ApolloServiceRegistryAutoConfiguration; +import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceDiscoveryProperties; +import com.ctrip.framework.apollo.biz.repository.ServiceRegistryRepository; +import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; +import java.util.List; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; + +/** + * test when {@link DatabaseDiscoveryClient} doesn't warp by decorator. + */ +@TestPropertySource( + properties = { + "apollo.service.registry.enabled=true", + "apollo.service.registry.cluster=default", + "apollo.service.discovery.enabled=true", + "spring.application.name=for-test-service", + "server.port=10000", + // close decorator + "ApolloServiceDiscoveryWithoutDecoratorAutoConfiguration.enabled=true", + } +) +@ContextConfiguration(classes = { + ApolloServiceRegistryAutoConfiguration.class, + // notice that the order of classes is import + // @AutoConfigureBefore(ApolloServiceDiscoveryAutoConfiguration.class) won't work when run test + ApolloServiceDiscoveryWithoutDecoratorAutoConfiguration.class, + ApolloServiceDiscoveryAutoConfiguration.class, +}) +@EnableJpaRepositories(basePackageClasses = ServiceRegistryRepository.class) +public class DatabaseDiscoveryWithoutDecoratorIntegrationTest extends AbstractIntegrationTest { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private DatabaseServiceRegistry serviceRegistry; + + @Autowired + private DatabaseDiscoveryClient discoveryClient; + + /** + * discover one after register, and delete it + */ + @Test + public void registerThenDiscoveryThenDelete() { + // register it + String serviceName = "a-service"; + String uri = "http://192.168.1.20:8080/"; + String cluster = "default"; + ServiceInstance instance = newServiceInstance( + serviceName, uri, cluster + ); + this.serviceRegistry.register(instance); + + // find it + List serviceInstances = this.discoveryClient.getInstances(serviceName); + assertEquals(1, serviceInstances.size()); + ServiceInstance actual = serviceInstances.get(0); + assertEquals(serviceName, actual.getServiceName()); + assertEquals(uri, actual.getUri().toString()); + assertEquals(cluster, actual.getCluster()); + assertEquals(0, actual.getMetadata().size()); + + // delete it + this.serviceRegistry.deregister(instance); + // find none + assertEquals(0, this.discoveryClient.getInstances(serviceName).size()); + } + + /** + * diff cluster so cannot be discover + */ + @Test + public void registerThenDiscoveryNone() { + // register it + String serviceName = "b-service"; + ServiceInstance instance = newServiceInstance( + serviceName, "http://192.168.1.20:8080/", "cannot-be-discovery" + ); + this.serviceRegistry.register(instance); + + // find none + List serviceInstances = this.discoveryClient.getInstances(serviceName); + assertEquals(0, serviceInstances.size()); + } + + @Test + public void registerTwice() { + String serviceName = "c-service"; + ServiceInstance instance = newServiceInstance( + serviceName, "http://192.168.1.20:8080/", "default" + ); + + // register it + this.serviceRegistry.register(instance); + // register again + this.serviceRegistry.register(instance); + + // only discover one + List serviceInstances = this.discoveryClient.getInstances(serviceName); + assertEquals(1, serviceInstances.size()); + } + + @Test + public void registerTwoInstancesThenDeleteOne() { + final String serviceName = "d-service"; + final String cluster = "default"; + + this.serviceRegistry.register( + newServiceInstance( + serviceName, "http://192.168.1.20:8080/", cluster + ) + ); + this.serviceRegistry.register( + newServiceInstance( + serviceName, "http://192.168.1.20:10000/", cluster + ) + ); + + final List serviceInstances = this.discoveryClient.getInstances(serviceName); + assertEquals(2, serviceInstances.size()); + + for (ServiceInstance serviceInstance : serviceInstances) { + assertEquals(serviceName, serviceInstance.getServiceName()); + assertEquals(cluster, serviceInstance.getCluster()); + assertEquals(0, serviceInstance.getMetadata().size()); + } + + // delete one + this.serviceRegistry.deregister( + newServiceInstance( + serviceName, "http://192.168.1.20:10000/", cluster + ) + ); + + assertEquals(1, this.discoveryClient.getInstances(serviceName).size()); + assertEquals("http://192.168.1.20:8080/", + this.discoveryClient.getInstances(serviceName).get(0).getUri().toString()); + } + + /** + * only use in {@link DatabaseDiscoveryWithoutDecoratorIntegrationTest} + */ + @Configuration + @ConditionalOnProperty(prefix = "ApolloServiceDiscoveryWithoutDecoratorAutoConfiguration", value = "enabled") + @ConditionalOnBean(ApolloServiceDiscoveryAutoConfiguration.class) + @AutoConfigureBefore(ApolloServiceDiscoveryAutoConfiguration.class) + @EnableConfigurationProperties({ + ApolloServiceDiscoveryProperties.class, + }) + static class ApolloServiceDiscoveryWithoutDecoratorAutoConfiguration { + @Bean + public DatabaseDiscoveryClient databaseDiscoveryClient( + ApolloServiceDiscoveryProperties discoveryProperties, + ServiceInstance selfServiceInstance, + ServiceRegistryService serviceRegistryService + ) { + return new DatabaseDiscoveryClientImpl( + serviceRegistryService, discoveryProperties, selfServiceInstance.getCluster() + ); + } + } +} diff --git a/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/ServiceInstanceFactory.java b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/ServiceInstanceFactory.java new file mode 100644 index 00000000000..fa4f4d2acf0 --- /dev/null +++ b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/ServiceInstanceFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry; + +import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryProperties; + +public class ServiceInstanceFactory { + static ServiceInstance newServiceInstance(String serviceName, String uri, String cluster) { + ApolloServiceRegistryProperties instance = new ApolloServiceRegistryProperties(); + instance.setServiceName(serviceName); + instance.setUri(uri); + instance.setCluster(cluster); + return instance; + } +} diff --git a/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/configuration/ApolloServiceRegistryAutoConfigurationNotEnabledTest.java b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/configuration/ApolloServiceRegistryAutoConfigurationNotEnabledTest.java new file mode 100644 index 00000000000..0efe88e6fa5 --- /dev/null +++ b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/configuration/ApolloServiceRegistryAutoConfigurationNotEnabledTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry.configuration; + +import com.ctrip.framework.apollo.biz.registry.DatabaseDiscoveryClient; +import com.ctrip.framework.apollo.biz.registry.DatabaseServiceRegistry; +import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryClearApplicationRunner; +import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryDeregisterApplicationListener; +import com.ctrip.framework.apollo.biz.registry.configuration.support.ApolloServiceRegistryHeartbeatApplicationRunner; +import com.ctrip.framework.apollo.biz.repository.ServiceRegistryRepository; +import com.ctrip.framework.apollo.biz.service.ServiceRegistryService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; + +/** + * ensure that this feature, i.e. database discovery won't cause configservice or adminservice + * startup fail when it doesn't enable. + */ +@SpringBootTest +@ContextConfiguration(classes = { + ApolloServiceRegistryAutoConfiguration.class, + ApolloServiceDiscoveryAutoConfiguration.class +}) +class ApolloServiceRegistryAutoConfigurationNotEnabledTest { + + @Autowired + private ApplicationContext context; + + + private void assertNoSuchBean(Class requiredType) { + Assertions.assertThrows( + NoSuchBeanDefinitionException.class, + () -> context.getBean(requiredType) + ); + } + + @Test + void ensureNoSuchBeans() { + assertNoSuchBean(ServiceRegistryRepository.class); + assertNoSuchBean(ServiceRegistryService.class); + assertNoSuchBean(DatabaseServiceRegistry.class); + assertNoSuchBean(ApolloServiceRegistryHeartbeatApplicationRunner.class); + assertNoSuchBean(ApolloServiceRegistryDeregisterApplicationListener.class); + + assertNoSuchBean(DatabaseDiscoveryClient.class); + assertNoSuchBean(ApolloServiceRegistryClearApplicationRunner.class); + } +} \ No newline at end of file diff --git a/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryClearApplicationRunnerIntegrationTest.java b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryClearApplicationRunnerIntegrationTest.java new file mode 100644 index 00000000000..50fedba6899 --- /dev/null +++ b/apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/configuration/support/ApolloServiceRegistryClearApplicationRunnerIntegrationTest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.biz.registry.configuration.support; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.ctrip.framework.apollo.biz.AbstractIntegrationTest; +import com.ctrip.framework.apollo.biz.entity.ServiceRegistry; +import com.ctrip.framework.apollo.biz.registry.configuration.ApolloServiceDiscoveryAutoConfiguration; +import com.ctrip.framework.apollo.biz.registry.configuration.ApolloServiceRegistryAutoConfiguration; +import com.ctrip.framework.apollo.biz.repository.ServiceRegistryRepository; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource( + properties = { + "apollo.service.registry.enabled=true", + "apollo.service.registry.cluster=default", + "apollo.service.discovery.enabled=true", + "spring.application.name=for-test-service", + "server.port=10000", + } +) +@ContextConfiguration(classes = { + ApolloServiceRegistryAutoConfiguration.class, + ApolloServiceDiscoveryAutoConfiguration.class, +}) +@EnableJpaRepositories(basePackageClasses = ServiceRegistryRepository.class) +public class ApolloServiceRegistryClearApplicationRunnerIntegrationTest + extends AbstractIntegrationTest { + + @Autowired + private ServiceRegistryRepository repository; + + @Autowired + private ApolloServiceRegistryClearApplicationRunner runner; + + @Test + public void clearUnhealthyInstances() { + final String serviceName = "h-service"; + + final String healthUri = "http://10.240.11.22:8080/"; + ServiceRegistry healthy = new ServiceRegistry(); + healthy.setServiceName(serviceName); + healthy.setCluster("c-1"); + healthy.setUri(healthUri); + healthy.setDataChangeCreatedTime(LocalDateTime.now()); + healthy.setDataChangeLastModifiedTime(LocalDateTime.now()); + this.repository.save(healthy); + + LocalDateTime unhealthyTime = LocalDateTime.now().minusDays(2L); + ServiceRegistry unhealthy = new ServiceRegistry(); + unhealthy.setServiceName("h-service"); + unhealthy.setCluster("c-2"); + unhealthy.setUri("http://10.240.33.44:9090/"); + unhealthy.setDataChangeCreatedTime(unhealthyTime); + unhealthy.setDataChangeLastModifiedTime(unhealthyTime); + this.repository.save(unhealthy); + + { + List serviceRegistryList = this.repository.findByServiceNameAndDataChangeLastModifiedTimeGreaterThan( + serviceName, + LocalDateTime.now().minusDays(3L) + ); + assertEquals(2, serviceRegistryList.size()); + } + + runner.clearUnhealthyInstances(); + + { + List serviceRegistryList = this.repository.findByServiceNameAndDataChangeLastModifiedTimeGreaterThan( + serviceName, + LocalDateTime.now().minusDays(3L) + ); + assertEquals(1, serviceRegistryList.size()); + ServiceRegistry registry = serviceRegistryList.get(0); + assertEquals(serviceName, registry.getServiceName()); + assertEquals(healthUri, registry.getUri()); + } + } + +} \ No newline at end of file diff --git a/apollo-biz/src/test/resources/json/converter/element.1.json b/apollo-biz/src/test/resources/json/converter/element.1.json new file mode 100644 index 00000000000..6e8023fb39e --- /dev/null +++ b/apollo-biz/src/test/resources/json/converter/element.1.json @@ -0,0 +1 @@ +{"a":"1"} \ No newline at end of file diff --git a/apollo-biz/src/test/resources/json/converter/element.2.json b/apollo-biz/src/test/resources/json/converter/element.2.json new file mode 100644 index 00000000000..de2bd72bb59 --- /dev/null +++ b/apollo-biz/src/test/resources/json/converter/element.2.json @@ -0,0 +1 @@ +{"a":"1","disableCheck":"true"} \ No newline at end of file diff --git a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/controller/HomePageController.java b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/controller/HomePageController.java index 3b88941a02d..1dbd4dddcf9 100644 --- a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/controller/HomePageController.java +++ b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/controller/HomePageController.java @@ -29,7 +29,14 @@ /** * For non-eureka discovery services such as kubernetes and nacos, there is no eureka home page, so we need to add a default one */ -@Profile({"kubernetes", "nacos-discovery", "consul-discovery", "zookeeper-discovery"}) +@Profile({ + "kubernetes", + "nacos-discovery", + "consul-discovery", + "zookeeper-discovery", + "custom-defined-discovery", + "database-discovery", +}) @RestController public class HomePageController { private final DiscoveryService discoveryService; diff --git a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/service/DatabaseDiscoveryService.java b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/service/DatabaseDiscoveryService.java new file mode 100644 index 00000000000..394e1a4b03b --- /dev/null +++ b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/service/DatabaseDiscoveryService.java @@ -0,0 +1,64 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.metaservice.service; + +import com.ctrip.framework.apollo.biz.registry.DatabaseDiscoveryClient; +import com.ctrip.framework.apollo.biz.registry.ServiceInstance; +import com.ctrip.framework.apollo.core.dto.ServiceDTO; +import java.util.ArrayList; +import java.util.List; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +/** + * use database as a registry + */ +@Service +@Profile("database-discovery") +public class DatabaseDiscoveryService implements DiscoveryService { + + private final DatabaseDiscoveryClient discoveryClient; + + public DatabaseDiscoveryService( + DatabaseDiscoveryClient discoveryClient) { + this.discoveryClient = discoveryClient; + } + + @Override + public List getServiceInstances(String serviceId) { + List serviceInstanceList = this.discoveryClient.getInstances(serviceId); + return convert(serviceInstanceList); + } + + static List convert(List list) { + List serviceDTOList = new ArrayList<>(list.size()); + for (ServiceInstance serviceInstance : list) { + ServiceDTO serviceDTO = convert(serviceInstance); + serviceDTOList.add(serviceDTO); + } + return serviceDTOList; + } + + static ServiceDTO convert(ServiceInstance serviceInstance) { + ServiceDTO serviceDTO = new ServiceDTO(); + serviceDTO.setAppName(serviceInstance.getServiceName()); + String homePageUrl = serviceInstance.getUri().toString(); + serviceDTO.setInstanceId(homePageUrl); + serviceDTO.setHomepageUrl(homePageUrl); + return serviceDTO; + } +} diff --git a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/service/DefaultDiscoveryService.java b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/service/DefaultDiscoveryService.java index ed10ce6b387..73582cfc514 100644 --- a/apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/service/DefaultDiscoveryService.java +++ b/apollo-configservice/src/main/java/com/ctrip/framework/apollo/metaservice/service/DefaultDiscoveryService.java @@ -33,7 +33,14 @@ * Default discovery service for Eureka */ @Service -@ConditionalOnMissingProfile({"kubernetes", "nacos-discovery", "consul-discovery", "zookeeper-discovery", "custom-defined-discovery"}) +@ConditionalOnMissingProfile({ + "kubernetes", + "nacos-discovery", + "consul-discovery", + "zookeeper-discovery", + "custom-defined-discovery", + "database-discovery", +}) public class DefaultDiscoveryService implements DiscoveryService { private final EurekaClient eurekaClient; diff --git a/apollo-configservice/src/main/resources/application-database-discovery.properties b/apollo-configservice/src/main/resources/application-database-discovery.properties new file mode 100644 index 00000000000..de8b15950ce --- /dev/null +++ b/apollo-configservice/src/main/resources/application-database-discovery.properties @@ -0,0 +1,26 @@ +# +# Copyright 2022 Apollo Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +apollo.eureka.server.enabled=false +eureka.client.enabled=false +spring.cloud.discovery.enabled=false + +apollo.service.registry.enabled=true +apollo.service.registry.cluster=default +apollo.service.registry.heartbeatIntervalInSecond=10 + +apollo.service.discovery.enabled=true +# health check by heartbeat, heartbeat time before 61s ago will be seemed as unhealthy +apollo.service.discovery.healthCheckIntervalInSecond = 61 diff --git a/docs/en/deployment/distributed-deployment-guide.md b/docs/en/deployment/distributed-deployment-guide.md index 46cd1ad43f6..d4d858c6883 100644 --- a/docs/en/deployment/distributed-deployment-guide.md +++ b/docs/en/deployment/distributed-deployment-guide.md @@ -583,6 +583,24 @@ apollo.config-service.url=http://apollo-config-service apollo.admin-service.url=http://apollo-admin-service ```` +##### 2.2.1.2.11 Enable database-discovery to replace built-in eureka + +> For version 2.1.0 and above +> +> Apollo supports the use of internal database table as registry, without relying on third-party registry. + +1. Modify build.sh/build.bat and change the maven compilation commands of `config-service` and `admin-service` to +```shell +mvn clean package -Pgithub -DskipTests -pl apollo-configservice,apollo-adminservice -am -Dapollo_profile=github,database-discovery -Dspring_datasource_url=$apollo_config_db_url -Dspring_datasource_username=$apollo_config_db_username -Dspring_datasource_password=$apollo_config_db_password +``` + +2. In multi-cluster deployments, if you want apollo client only read Config Service in the same cluster, +you can add a property in `config/application-github.properties` of the Config Service and Admin Service installation package +```properties +apollo.service.registry.cluster=same name with apollo Cluster +``` + + ### 2.2.2 Deploy Apollo server #### 2.2.2.1 Deploy apollo-configservice @@ -1343,7 +1361,7 @@ http://5.5.5.5:8080/eureka/,http://6.6.6.6:8080/eureka/ >Note 2: If you want to register Config Service and Admin Service to the company's unified Eureka, you can refer to [Deployment & Development FAQ - Registering Config Service and Admin Service to a separate Eureka Server](en/faq/common-issues-in-deployment-and-development-phase?id=_8-register-config-service-and-admin-service-to-a-separate-eureka-server) section ->Note 3: In multi-room deployments, you often want the config service and admin service to register only with the eureka in the same room. To achieve this, you need to use the cluster field in the `ServerConfig` table, and the config service and admin service will read the `/opt/settings/server.properties` (Mac/Linux) or `C:\opt\settings\server.properties` (Windows), and if the idc has a corresponding eureka.service.url configuration, then will only register with eureka for that server room. For example, if the config service and admin service are deployed to two IDCs, `SHAOY` and `SHAJQ`, then in order to register the services in these two server rooms only with that server room, you can add two new records in the `ServerConfig` table and fill in the `SHAOY` and `SHAJQ` server room eureka addresses respectively. If there are config service and admin service that are not deployed in `SHAOY` and `SHAJQ`, this default configuration will be used. +>Note 3: In multi-cluster deployments, you often want the config service and admin service to register only with the eureka in the same room. To achieve this, you need to use the cluster field in the `ServerConfig` table, and the config service and admin service will read the `/opt/settings/server.properties` (Mac/Linux) or `C:\opt\settings\server.properties` (Windows), and if the idc has a corresponding eureka.service.url configuration, then will only register with eureka for that server room. For example, if the config service and admin service are deployed to two IDCs, `SHAOY` and `SHAJQ`, then in order to register the services in these two server rooms only with that server room, you can add two new records in the `ServerConfig` table and fill in the `SHAOY` and `SHAJQ` server room eureka addresses respectively. If there are config service and admin service that are not deployed in `SHAOY` and `SHAJQ`, this default configuration will be used. | Key | Cluster | Value | Comment | | ------------------ | ------- | --------------------------- | ---------------------------- | diff --git a/docs/zh/deployment/distributed-deployment-guide.md b/docs/zh/deployment/distributed-deployment-guide.md index cc91d1200ca..8ed8c226fad 100644 --- a/docs/zh/deployment/distributed-deployment-guide.md +++ b/docs/zh/deployment/distributed-deployment-guide.md @@ -544,6 +544,25 @@ INSERT INTO `ApolloConfigDB`.`ServerConfig` (`Key`, `Value`, `Comment`) VALUES ( apollo.config-service.url=http://apollo-config-service apollo.admin-service.url=http://apollo-admin-service ``` + +##### 2.2.1.2.11 启用database-discovery替换内置eureka + +> For version 2.1.0 and above +> +> Apollo支持使用内部的数据库表作为注册中心,不依赖第三方的注册中心 + +1. 修改build.sh/build.bat,将`config-service`和`admin-service`的maven编译命令更改为 +```shell +mvn clean package -Pgithub -DskipTests -pl apollo-configservice,apollo-adminservice -am -Dapollo_profile=github,database-discovery -Dspring_datasource_url=$apollo_config_db_url -Dspring_datasource_username=$apollo_config_db_username -Dspring_datasource_password=$apollo_config_db_password +``` + +2. 在多机房部署时, +如果你需要apollo客户端只读取同机房内的Config Service, +你可以在Config Service和Admin Service安装包中`config/application-github.properties`新增一条配置 +```properties +apollo.service.registry.cluster=与apollo的Cluster同名 +``` + ### 2.2.2 部署Apollo服务端 #### 2.2.2.1 部署apollo-configservice diff --git a/scripts/sql/apolloconfigdb.sql b/scripts/sql/apolloconfigdb.sql index 05076dfaf09..54bec8e6cc9 100644 --- a/scripts/sql/apolloconfigdb.sql +++ b/scripts/sql/apolloconfigdb.sql @@ -410,6 +410,26 @@ CREATE TABLE `AccessKey` ( KEY `DataChange_LastTime` (`DataChange_LastTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='访问密钥'; + +# Dump of table serviceregistry +# ------------------------------------------------------------ + +DROP TABLE IF EXISTS `ServiceRegistry`; + +CREATE TABLE `ServiceRegistry` ( + `Id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增Id', + `ServiceName` VARCHAR(64) NOT NULL COMMENT '服务名', + `Uri` VARCHAR(64) NOT NULL COMMENT '服务地址', + `Cluster` VARCHAR(64) NOT NULL COMMENT '集群,可以用来标识apollo.cluster或者网络分区', + `Metadata` VARCHAR(1024) NOT NULL DEFAULT '{}' COMMENT '元数据,key value结构的json object,为了方面后面扩展功能而不需要修改表结构', + `DataChange_CreatedTime` TIMESTAMP NOT NULL COMMENT '创建时间', + `DataChange_LastTime` TIMESTAMP NOT NULL COMMENT '最后修改时间', + PRIMARY KEY (`Id`), + UNIQUE INDEX `IX_UNIQUE_KEY` (`ServiceName`, `Uri`), + INDEX `IX_DataChange_LastTime` (`DataChange_LastTime`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='注册中心'; + + # Config # ------------------------------------------------------------ INSERT INTO `ServerConfig` (`Key`, `Cluster`, `Value`, `Comment`) diff --git a/scripts/sql/delta/v210-v220/apolloconfigdb-v210-v220.sql b/scripts/sql/delta/v210-v220/apolloconfigdb-v210-v220.sql new file mode 100644 index 00000000000..78e686abd8b --- /dev/null +++ b/scripts/sql/delta/v210-v220/apolloconfigdb-v210-v220.sql @@ -0,0 +1,31 @@ +-- +-- Copyright 2022 Apollo Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +# delta schema to upgrade apollo config db from v2.1.0 to v2.2.0 + +Use ApolloConfigDB; + +CREATE TABLE `ServiceRegistry` ( + `Id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增Id', + `ServiceName` VARCHAR(64) NOT NULL COMMENT '服务名', + `Uri` VARCHAR(64) NOT NULL COMMENT '服务地址', + `Cluster` VARCHAR(64) NOT NULL COMMENT '集群,可以用来标识apollo.cluster或者网络分区', + `Metadata` VARCHAR(1024) NOT NULL DEFAULT '{}' COMMENT '元数据,key value结构的json object,为了方面后面扩展功能而不需要修改表结构', + `DataChange_CreatedTime` TIMESTAMP NOT NULL COMMENT '创建时间', + `DataChange_LastTime` TIMESTAMP NOT NULL COMMENT '最后修改时间', + PRIMARY KEY (`Id`), + UNIQUE INDEX `IX_UNIQUE_KEY` (`ServiceName`, `Uri`), + INDEX `IX_DataChange_LastTime` (`DataChange_LastTime`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='注册中心'; \ No newline at end of file