This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new dd8b31240d fix(admin): preserve discovered upstreams across stale
health checks (#7882)
dd8b31240d is described below
commit dd8b31240d7397babad34d6dda2f6e6afe3c94bf
Author: cloudbase-ai <[email protected]>
AuthorDate: Wed Oct 21 10:17:17 2026 +0800
fix(admin): preserve discovered upstreams across stale health checks (#7882)
* fix(admin): preserve discovered upstreams across stale health checks
* fix(admin): address upstream snapshot review feedback
Signed-off-by: cloudbase-ai <[email protected]>
---------
Signed-off-by: cloudbase-ai <[email protected]>
Co-authored-by: cloudbase-ai
<[email protected]>
---
.../DiscoveryDataChangedEventSyncListener.java | 24 ++
.../admin/service/DiscoveryUpstreamService.java | 8 +
.../service/impl/DiscoveryUpstreamServiceImpl.java | 5 +
.../admin/service/impl/UpstreamCheckService.java | 156 ++++++++-----
.../DiscoveryDataChangedEventSyncListenerTest.java | 166 +++++++++++++
.../service/DiscoveryUpstreamServiceTest.java | 8 +
.../service/UpstreamCheckServiceRecoveryTest.java | 3 +-
.../service/UpstreamCheckServiceSyncTest.java | 257 ++++++++++++++++++++-
8 files changed, 563 insertions(+), 64 deletions(-)
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/discovery/DiscoveryDataChangedEventSyncListener.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/discovery/DiscoveryDataChangedEventSyncListener.java
index 2b7b7c5b93..30778fc842 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/discovery/DiscoveryDataChangedEventSyncListener.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/discovery/DiscoveryDataChangedEventSyncListener.java
@@ -25,6 +25,8 @@ import org.apache.shenyu.admin.discovery.parse.KeyValueParser;
import org.apache.shenyu.admin.listener.DataChangedEvent;
import org.apache.shenyu.admin.mapper.DiscoveryUpstreamMapper;
import org.apache.shenyu.admin.model.entity.DiscoveryUpstreamDO;
+import org.apache.shenyu.admin.model.enums.EventTypeEnum;
+import
org.apache.shenyu.admin.model.event.discovery.DiscoveryStreamUpdatedEvent;
import org.apache.shenyu.admin.transfer.DiscoveryTransfer;
import org.apache.shenyu.common.dto.DiscoverySyncData;
import org.apache.shenyu.common.dto.DiscoveryUpstreamData;
@@ -36,6 +38,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronization;
+import
org.springframework.transaction.support.TransactionSynchronizationManager;
import java.sql.Timestamp;
import java.util.Collections;
@@ -135,10 +139,30 @@ public class DiscoveryDataChangedEventSyncListener
implements DataChangedEventLi
}
syncData.setUpstreamDataList(discoveryUpstreamMapper.selectByDiscoveryHandlerId(discoveryHandlerId).stream()
.map(DiscoveryTransfer.INSTANCE::mapToData).collect(Collectors.toList()));
+ if (TransactionSynchronizationManager.isActualTransactionActive()
+ &&
TransactionSynchronizationManager.isSynchronizationActive()) {
+ TransactionSynchronizationManager.registerSynchronization(new
TransactionSynchronization() {
+ @Override
+ public void afterCommit() {
+ refreshHealthSnapshot(syncData);
+ }
+ });
+ } else {
+ refreshHealthSnapshot(syncData);
+ }
DataChangedEvent dataChangedEvent = new
DataChangedEvent(ConfigGroupEnum.DISCOVER_UPSTREAM, DataEventTypeEnum.UPDATE,
Collections.singletonList(syncData));
eventPublisher.publishEvent(dataChangedEvent);
}
+ private void refreshHealthSnapshot(final DiscoverySyncData syncData) {
+ try {
+ eventPublisher.publishEvent(new
DiscoveryStreamUpdatedEvent(syncData, EventTypeEnum.UPDATE));
+ } catch (RuntimeException ex) {
+ // A local refresh failure must not prevent delivery of the
committed discovery snapshot.
+ LOG.warn("Failed to refresh health-check upstreams for selector
{}", syncData.getSelectorId(), ex);
+ }
+ }
+
private DiscoverySyncData buildProxySelectorData(final DiscoverySyncData
discoverySyncData, final String value) {
List<DiscoveryUpstreamData> discoveryUpstreamDTOS =
keyValueParser.parseValue(value);
discoveryUpstreamDTOS.forEach(discoveryUpstreamData -> {
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DiscoveryUpstreamService.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DiscoveryUpstreamService.java
index b99a51bda3..e1587419dc 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DiscoveryUpstreamService.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DiscoveryUpstreamService.java
@@ -114,6 +114,14 @@ public interface DiscoveryUpstreamService {
*/
List<DiscoveryUpstreamData> findBySelectorId(String selectorId);
+ /**
+ * Whether a selector has authoritative discovery state, including an
empty instance list.
+ *
+ * @param selectorId selector id
+ * @return whether a discovery handler exists
+ */
+ boolean hasDiscoveryHandler(String selectorId);
+
/**
* deleteBySelectorIdAndUrl.
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
index 0004cb43c0..792ac17d67 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
@@ -323,6 +323,11 @@ public class DiscoveryUpstreamServiceImpl implements
DiscoveryUpstreamService {
return
discoveryUpstreamDOS.stream().map(DiscoveryTransfer.INSTANCE::mapToData).collect(Collectors.toList());
}
+ @Override
+ public boolean hasDiscoveryHandler(final String selectorId) {
+ return
Objects.nonNull(discoveryHandlerMapper.selectBySelectorId(selectorId));
+ }
+
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteBySelectorIdAndUrl(final String selectorId, final String
url) {
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/UpstreamCheckService.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/UpstreamCheckService.java
index b7715a6fec..c3ed957de3 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/UpstreamCheckService.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/UpstreamCheckService.java
@@ -20,6 +20,7 @@ package org.apache.shenyu.admin.service.impl;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Striped;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.StringUtils;
@@ -49,7 +50,6 @@ import org.apache.shenyu.common.enums.ConfigGroupEnum;
import org.apache.shenyu.common.enums.DataEventTypeEnum;
import org.apache.shenyu.common.enums.PluginEnum;
import org.apache.shenyu.common.utils.GsonUtils;
-import org.apache.shenyu.common.utils.MapUtils;
import org.apache.shenyu.common.utils.UpstreamCheckUtils;
import org.apache.shenyu.register.common.config.ShenyuRegisterCenterConfig;
import org.slf4j.Logger;
@@ -73,6 +73,7 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
import java.util.stream.Collectors;
/**
@@ -85,6 +86,8 @@ public class UpstreamCheckService {
private static final Map<String, List<CommonUpstream>> UPSTREAM_MAP =
Maps.newConcurrentMap();
+ private static final Striped<Lock> UPSTREAM_UPDATE_LOCKS =
Striped.lazyWeakLock(1024);
+
private static final Set<Integer> PENDING_SYNC =
Sets.newConcurrentHashSet();
private static final Set<ZombieUpstream> ZOMBIE_SET =
Sets.newConcurrentHashSet();
@@ -194,7 +197,13 @@ public class UpstreamCheckService {
* @param selectorId the selector id
*/
public static void removeByKey(final String selectorId) {
- UPSTREAM_MAP.remove(selectorId);
+ Lock lock = UPSTREAM_UPDATE_LOCKS.get(selectorId);
+ lock.lock();
+ try {
+ UPSTREAM_MAP.remove(selectorId);
+ } finally {
+ lock.unlock();
+ }
}
/**
@@ -209,7 +218,7 @@ public class UpstreamCheckService {
}
Optional.ofNullable(submitJust(selectorId, commonUpstream))
- .ifPresent(upstreams -> executor.execute(() ->
updateHandler(selectorId, upstreams, upstreams)));
+ .ifPresent(upstreams -> executor.execute(() ->
updateHandler(selectorId, upstreams, upstreams, upstreams)));
}
private List<CommonUpstream> submitJust(final String selectorId, final
CommonUpstream commonUpstream) {
@@ -217,23 +226,23 @@ public class UpstreamCheckService {
return null;
}
- List<CommonUpstream> upstreams =
MapUtils.computeIfAbsent(UPSTREAM_MAP, selectorId, k -> new
CopyOnWriteArrayList<>());
- if (commonUpstream.isStatus()) {
- Optional<CommonUpstream> exists = upstreams.stream().filter(item
-> StringUtils.isNotBlank(item.getUpstreamUrl())
- &&
item.getUpstreamUrl().equals(commonUpstream.getUpstreamUrl())).findFirst();
- if (!exists.isPresent()) {
- upstreams.add(commonUpstream);
+ return UPSTREAM_MAP.compute(selectorId, (key, current) -> {
+ List<CommonUpstream> upstreams = Objects.isNull(current) ? new
CopyOnWriteArrayList<>() : new CopyOnWriteArrayList<>(current);
+ if (commonUpstream.isStatus()) {
+ Optional<CommonUpstream> exists =
upstreams.stream().filter(item -> StringUtils.isNotBlank(item.getUpstreamUrl())
+ &&
item.getUpstreamUrl().equals(commonUpstream.getUpstreamUrl())).findFirst();
+ if (!exists.isPresent()) {
+ upstreams.add(commonUpstream);
+ } else {
+
exists.get().setHealthCheckEnabled(commonUpstream.isHealthCheckEnabled());
+ }
+ PENDING_SYNC.add(commonUpstream.hashCode());
} else {
- CommonUpstream existUpstream = exists.get();
-
existUpstream.setHealthCheckEnabled(commonUpstream.isHealthCheckEnabled());
- LOG.info("upstream host {} is exists.",
commonUpstream.getUpstreamHost());
+ upstreams.removeIf(item -> item.equals(commonUpstream));
+ PENDING_SYNC.add(NumberUtils.INTEGER_ZERO);
}
- PENDING_SYNC.add(commonUpstream.hashCode());
- } else {
- upstreams.removeIf(item -> item.equals(commonUpstream));
- PENDING_SYNC.add(NumberUtils.INTEGER_ZERO);
- }
- return upstreams;
+ return upstreams;
+ });
}
/**
@@ -281,7 +290,13 @@ public class UpstreamCheckService {
if (!REGISTER_TYPE_HTTP.equalsIgnoreCase(registerType)) {
return;
}
- UPSTREAM_MAP.put(selectorId, toThreadSafeList(commonUpstreams));
+ Lock lock = UPSTREAM_UPDATE_LOCKS.get(selectorId);
+ lock.lock();
+ try {
+ UPSTREAM_MAP.put(selectorId, toThreadSafeList(commonUpstreams));
+ } finally {
+ lock.unlock();
+ }
}
private void scheduled() {
@@ -349,8 +364,9 @@ public class UpstreamCheckService {
}
private void check(final String selectorId, final List<CommonUpstream>
upstreamList) {
- final List<CompletableFuture<CommonUpstream>> checkFutures = new
ArrayList<>(upstreamList.size());
- for (CommonUpstream commonUpstream : upstreamList) {
+ final List<CommonUpstream> checkedUpstreams = new
ArrayList<>(upstreamList);
+ final List<CompletableFuture<CommonUpstream>> checkFutures = new
ArrayList<>(checkedUpstreams.size());
+ for (CommonUpstream commonUpstream : checkedUpstreams) {
checkFutures.add(CompletableFuture.supplyAsync(() -> {
if (!commonUpstream.isHealthCheckEnabled()) {
if (!commonUpstream.isStatus()) {
@@ -388,22 +404,44 @@ public class UpstreamCheckService {
.map(CompletableFuture::join)
.filter(Objects::nonNull)
.collect(Collectors.toList());
- updateHandler(selectorId, upstreamList, successList);
+ updateHandler(selectorId, checkedUpstreams, successList,
upstreamList);
}));
}
private void updateHandler(final String selectorId, final
List<CommonUpstream> upstreamList, final List<CommonUpstream> successList) {
+ updateHandler(selectorId, upstreamList, successList, null);
+ }
+
+ private void updateHandler(final String selectorId, final
List<CommonUpstream> upstreamList,
+ final List<CommonUpstream> successList, final
List<CommonUpstream> expectedUpstreams) {
//No node changes, including zombie node resurrection and live node
death
if (successList.size() == upstreamList.size() &&
PENDING_SYNC.isEmpty()) {
return;
}
- removePendingSync(successList);
- if (!successList.isEmpty()) {
- UPSTREAM_MAP.put(selectorId, toThreadSafeList(successList));
- updateSelectorHandler(selectorId, successList);
- } else {
- UPSTREAM_MAP.remove(selectorId);
- updateSelectorHandler(selectorId, new ArrayList<>());
+ // Serialize discovery replacement with publication, without holding a
ConcurrentHashMap bin lock during I/O.
+ Lock lock = UPSTREAM_UPDATE_LOCKS.get(selectorId);
+ lock.lock();
+ try {
+ List<CommonUpstream> current = UPSTREAM_MAP.get(selectorId);
+ // A registry replacement or removal supersedes an in-flight
health check.
+ if (Objects.nonNull(expectedUpstreams) && current !=
expectedUpstreams) {
+ return;
+ }
+ removePendingSync(successList);
+ List<CommonUpstream> alive = new ArrayList<>(successList);
+ if (Objects.nonNull(expectedUpstreams)) {
+ current.stream().filter(upstream ->
upstreamList.stream().noneMatch(checkedUpstream ->
+
checkedUpstream.getUpstreamUrl().equals(upstream.getUpstreamUrl())))
+ .filter(CommonUpstream::isStatus).forEach(alive::add);
+ }
+ Set<String> checkedFailedUrls =
upstreamList.stream().map(CommonUpstream::getUpstreamUrl).collect(Collectors.toSet());
+ successList.forEach(upstream ->
checkedFailedUrls.remove(upstream.getUpstreamUrl()));
+ List<CommonUpstream> reconciled =
updateSelectorHandler(selectorId, alive, checkedFailedUrls);
+ // A registration can update the cache during publication; retain
it for its queued update.
+ UPSTREAM_MAP.compute(selectorId, (key, latest) -> latest == current
+ ? (reconciled.isEmpty() ? null :
toThreadSafeList(reconciled)) : latest);
+ } finally {
+ lock.unlock();
}
}
@@ -416,18 +454,39 @@ public class UpstreamCheckService {
return upstreams instanceof CopyOnWriteArrayList ? upstreams : new
CopyOnWriteArrayList<>(upstreams);
}
- private void updateSelectorHandler(final String selectorId, final
List<CommonUpstream> aliveList) {
+ private List<CommonUpstream> updateSelectorHandler(final String
selectorId, final List<CommonUpstream> aliveList, final Set<String>
checkedFailedUrls) {
SelectorDO selectorDO = selectorMapper.selectById(selectorId);
if (Objects.isNull(selectorDO)) {
- return;
+ return aliveList;
}
PluginDO pluginDO = pluginMapper.selectById(selectorDO.getPluginId());
if (Objects.isNull(pluginDO)) {
- return;
+ return aliveList;
+ }
+
+ List<DiscoveryUpstreamData> discoveryUpstreamDataList =
discoveryUpstreamService.findBySelectorId(selectorId);
+ if (CollectionUtils.isEmpty(discoveryUpstreamDataList) &&
!discoveryUpstreamService.hasDiscoveryHandler(selectorId)) {
+ // Legacy registrations without discovery records still use their
checked upstreams.
+ discoveryUpstreamDataList =
aliveList.stream().map(DiscoveryTransfer.INSTANCE::mapToDiscoveryUpstreamData).collect(Collectors.toList());
}
+ Set<String> aliveUrls =
aliveList.stream().map(CommonUpstream::getUpstreamUrl).collect(Collectors.toSet());
+ discoveryUpstreamDataList.stream().filter(upstream ->
aliveUrls.contains(upstream.getUrl())).forEach(upstream -> {
+ LOG.info("change alive selectorId={}|url={}", selectorId,
upstream.getUrl());
+
discoveryUpstreamService.changeStatusBySelectorIdAndUrl(selectorId,
upstream.getUrl(), Boolean.TRUE);
+ upstream.setStatus(NumberUtils.INTEGER_ZERO);
+ });
+ // Unchecked instances retain their persisted status; a stale check is
not a full discovery snapshot.
+ discoveryUpstreamDataList = discoveryUpstreamDataList.stream()
+ .filter(upstream -> upstream.getStatus() == 0 &&
!checkedFailedUrls.contains(upstream.getUrl())).collect(Collectors.toList());
+ List<CommonUpstream> currentAlive =
discoveryUpstreamDataList.stream().map(upstream -> {
+ CommonUpstream common = aliveList.stream().filter(alive ->
alive.getUpstreamUrl().equals(upstream.getUrl())).findFirst()
+ .orElseGet(() ->
DiscoveryTransfer.INSTANCE.mapToCommonUpstream(upstream));
+ common.setStatus(true);
+ return common;
+ }).collect(Collectors.toList());
String pluginName = pluginDO.getName();
- String handler =
converterFactor.newInstance(pluginName).handler(selectorDO.getHandle(),
aliveList);
+ String handler =
converterFactor.newInstance(pluginName).handler(selectorDO.getHandle(),
currentAlive);
selectorDO.setHandle(handler);
selectorMapper.updateSelective(selectorDO);
@@ -440,28 +499,6 @@ public class UpstreamCheckService {
eventPublisher.publishEvent(new
DataChangedEvent(ConfigGroupEnum.SELECTOR, DataEventTypeEnum.UPDATE,
Collections.singletonList(selectorData)));
// publish discovery change event.
- List<DiscoveryUpstreamData> discoveryUpstreamDataList =
discoveryUpstreamService.findBySelectorId(selectorId);
-
- if (CollectionUtils.isEmpty(discoveryUpstreamDataList)) {
- discoveryUpstreamDataList =
aliveList.stream().map(DiscoveryTransfer.INSTANCE::mapToDiscoveryUpstreamData).collect(Collectors.toList());
- }
-
- discoveryUpstreamDataList.removeIf(u -> {
- for (CommonUpstream alive : aliveList) {
- if (alive.getUpstreamUrl().equals(u.getUrl())) {
- return false;
- }
- }
- return true;
- });
- // change live node status to TRUE
- discoveryUpstreamDataList.forEach(upstream -> {
- LOG.info("change alive selectorId={}|url={}", selectorId,
upstream.getUrl());
-
discoveryUpstreamService.changeStatusBySelectorIdAndUrl(selectorId,
upstream.getUrl(), Boolean.TRUE);
- // The snapshot was read before the database update; publish the
same healthy status.
- upstream.setStatus(NumberUtils.INTEGER_ZERO);
- });
-
DiscoverySyncData discoverySyncData = new DiscoverySyncData();
discoverySyncData.setUpstreamDataList(discoveryUpstreamDataList);
discoverySyncData.setPluginName(pluginName);
@@ -470,6 +507,7 @@ public class UpstreamCheckService {
discoverySyncData.setNamespaceId(selectorDO.getNamespaceId());
LOG.debug("UpstreamCacheManager update selectorId={}|json={}",
selectorId, GsonUtils.getGson().toJson(discoverySyncData));
eventPublisher.publishEvent(new
DataChangedEvent(ConfigGroupEnum.DISCOVER_UPSTREAM, DataEventTypeEnum.UPDATE,
Collections.singletonList(discoverySyncData)));
+ return currentAlive;
}
/**
@@ -499,7 +537,7 @@ public class UpstreamCheckService {
.collect(Collectors.toList()));
}
if (CollectionUtils.isNotEmpty(commonUpstreams)) {
- UPSTREAM_MAP.put(selectorDO.getId(), commonUpstreams);
+ replace(selectorDO.getId(), commonUpstreams);
PENDING_SYNC.add(NumberUtils.INTEGER_ZERO);
}
});
@@ -516,7 +554,11 @@ public class UpstreamCheckService {
LOG.info("onDiscoveryUpstreamUpdated plugin={}|list={}",
discoverySyncData.getPluginName(), discoverySyncData.getUpstreamDataList());
if
(PluginEnum.DIVIDE.getName().equals(discoverySyncData.getPluginName())) {
List<DiscoveryUpstreamData> upstreamDataList =
discoverySyncData.getUpstreamDataList();
- List<CommonUpstream> collect =
upstreamDataList.stream().map(DiscoveryTransfer.INSTANCE::mapToCommonUpstream).collect(Collectors.toList());
+ List<CommonUpstream> collect =
upstreamDataList.stream().map(upstream -> {
+ CommonUpstream common =
DiscoveryTransfer.INSTANCE.mapToCommonUpstream(upstream);
+ common.setStatus(upstream.getStatus() == 0);
+ return common;
+ }).collect(Collectors.toList());
List<CommonUpstream> commonUpstreams =
CommonUpstreamUtils.convertCommonUpstreamList(collect);
LOG.info("UpstreamCacheManager replace selectorId={}|json={}",
discoverySyncData.getSelectorId(), GsonUtils.getGson().toJson(commonUpstreams));
replace(discoverySyncData.getSelectorId(), commonUpstreams);
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/discovery/DiscoveryDataChangedEventSyncListenerTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/discovery/DiscoveryDataChangedEventSyncListenerTest.java
index 1a4286f533..945aecd5ba 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/discovery/DiscoveryDataChangedEventSyncListenerTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/discovery/DiscoveryDataChangedEventSyncListenerTest.java
@@ -17,13 +17,21 @@
package org.apache.shenyu.admin.discovery;
+import org.apache.shenyu.admin.config.properties.ClusterProperties;
import org.apache.shenyu.admin.discovery.listener.DiscoveryDataChangedEvent;
import org.apache.shenyu.admin.discovery.parse.KeyValueParser;
+import org.apache.shenyu.admin.listener.ClusterDataChangedEventForwarder;
import org.apache.shenyu.admin.listener.DataChangedEvent;
+import org.apache.shenyu.admin.listener.DataChangedEventDispatcher;
+import org.apache.shenyu.admin.listener.DataChangedListener;
import org.apache.shenyu.admin.mapper.DiscoveryUpstreamMapper;
import org.apache.shenyu.admin.model.entity.DiscoveryUpstreamDO;
+import org.apache.shenyu.admin.model.enums.EventTypeEnum;
+import
org.apache.shenyu.admin.model.event.discovery.DiscoveryStreamUpdatedEvent;
+import org.apache.shenyu.admin.service.manager.LoadServiceDocEntry;
import org.apache.shenyu.common.dto.DiscoverySyncData;
import org.apache.shenyu.common.dto.DiscoveryUpstreamData;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -34,11 +42,18 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.transaction.support.TransactionSynchronization;
+import
org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -47,7 +62,12 @@ import java.util.concurrent.TimeUnit;
import static
org.apache.shenyu.common.constant.Constants.SYS_DEFAULT_NAMESPACE_ID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -76,6 +96,152 @@ public class DiscoveryDataChangedEventSyncListenerTest {
discoveryDataChangedEventSyncListener = new
DiscoveryDataChangedEventSyncListener(eventPublisher, discoveryUpstreamMapper,
keyValueParser, contextInfo, discoveryHandlerId);
}
+ @AfterEach
+ void clearTransaction() {
+ if (TransactionSynchronizationManager.isSynchronizationActive()) {
+ TransactionSynchronizationManager.clearSynchronization();
+ }
+ TransactionSynchronizationManager.setActualTransactionActive(false);
+ }
+
+ @Test
+ void testCommittedRegistryChangeRefreshesCompleteHealthSnapshot() {
+ prepareRegistryChange();
+ DiscoveryUpstreamDO old = new DiscoveryUpstreamDO();
+ old.setUpstreamUrl("127.0.0.1:8080");
+ DiscoveryUpstreamDO replacement = new DiscoveryUpstreamDO();
+ replacement.setUpstreamUrl("127.0.0.1:8081");
+
when(discoveryUpstreamMapper.selectByDiscoveryHandlerId("discoveryHandlerId")).thenReturn(List.of(old,
replacement));
+ TransactionSynchronizationManager.setActualTransactionActive(true);
+ TransactionSynchronizationManager.initSynchronization();
+
+ discoveryDataChangedEventSyncListener.onChange(new
DiscoveryDataChangedEvent("key", "value",
DiscoveryDataChangedEvent.Event.ADDED));
+
+ verify(eventPublisher,
never()).publishEvent(any(DiscoveryStreamUpdatedEvent.class));
+
TransactionSynchronizationManager.getSynchronizations().forEach(TransactionSynchronization::afterCommit);
+ ArgumentCaptor<DiscoveryStreamUpdatedEvent> captor =
ArgumentCaptor.forClass(DiscoveryStreamUpdatedEvent.class);
+ verify(eventPublisher).publishEvent(captor.capture());
+ DiscoverySyncData snapshot = captor.getValue().getDiscoverySyncData();
+ Assertions.assertEquals("selector-1", snapshot.getSelectorId());
+ Assertions.assertEquals(2, snapshot.getUpstreamDataList().size());
+ Assertions.assertEquals("127.0.0.1:8081",
snapshot.getUpstreamDataList().get(1).getUrl());
+ }
+
+ @Test
+ void testRolledBackRegistryChangeDoesNotRefreshHealthSnapshot() {
+ prepareRegistryChange();
+ TransactionSynchronizationManager.setActualTransactionActive(true);
+ TransactionSynchronizationManager.initSynchronization();
+
+ discoveryDataChangedEventSyncListener.onChange(new
DiscoveryDataChangedEvent("key", "value",
DiscoveryDataChangedEvent.Event.ADDED));
+ TransactionSynchronizationManager.getSynchronizations()
+ .forEach(synchronization ->
synchronization.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK));
+
+ verify(eventPublisher,
never()).publishEvent(any(DiscoveryStreamUpdatedEvent.class));
+ }
+
+ @Test
+ void testHealthRefreshPrecedesCommittedGatewayDispatch() {
+ prepareRegistryChange();
+ List<String> delivered = new ArrayList<>();
+ TransactionSynchronizationManager.setActualTransactionActive(true);
+ TransactionSynchronizationManager.initSynchronization();
+ connectGatewayDispatcher(delivered);
+
+ discoveryDataChangedEventSyncListener.onChange(new
DiscoveryDataChangedEvent("key", "value",
DiscoveryDataChangedEvent.Event.ADDED));
+ Assertions.assertTrue(delivered.isEmpty());
+
TransactionSynchronizationManager.getSynchronizations().forEach(TransactionSynchronization::afterCommit);
+
+ Assertions.assertEquals(List.of("health", "gateway"), delivered);
+ }
+
+ @Test
+ void testHealthRefreshFailureDoesNotSuppressCommittedGatewayDispatch() {
+ prepareRegistryChange();
+ List<String> delivered = new ArrayList<>();
+ TransactionSynchronizationManager.setActualTransactionActive(true);
+ TransactionSynchronizationManager.initSynchronization();
+ connectGatewayDispatcher(delivered);
+ doThrow(new IllegalArgumentException("invalid discovery properties"))
+
.when(eventPublisher).publishEvent(any(DiscoveryStreamUpdatedEvent.class));
+
+ discoveryDataChangedEventSyncListener.onChange(new
DiscoveryDataChangedEvent("key", "value",
DiscoveryDataChangedEvent.Event.ADDED));
+
+ Assertions.assertDoesNotThrow(() ->
TransactionSynchronizationManager.getSynchronizations().forEach(TransactionSynchronization::afterCommit));
+ Assertions.assertEquals(Collections.singletonList("gateway"),
delivered);
+ }
+
+ @Test
+ void testHealthRefreshFailureDoesNotSuppressNonTransactionalGatewayEvent()
{
+ prepareRegistryChange();
+ doThrow(new IllegalArgumentException("invalid discovery properties"))
+
.when(eventPublisher).publishEvent(any(DiscoveryStreamUpdatedEvent.class));
+
+ Assertions.assertDoesNotThrow(() ->
discoveryDataChangedEventSyncListener.onChange(
+ new DiscoveryDataChangedEvent("key", "value",
DiscoveryDataChangedEvent.Event.UPDATED)));
+
+ verify(eventPublisher).publishEvent(any(DataChangedEvent.class));
+ }
+
+ @SuppressWarnings("unchecked")
+ private void connectGatewayDispatcher(final List<String> delivered) {
+ ApplicationContext context = mock(ApplicationContext.class);
+ DataChangedListener gateway = mock(DataChangedListener.class);
+
when(context.getBeansOfType(DataChangedListener.class)).thenReturn(Map.of("gateway",
gateway));
+
when(context.getBean(LoadServiceDocEntry.class)).thenReturn(mock(LoadServiceDocEntry.class));
+ ObjectProvider<ClusterDataChangedEventForwarder> forwarder =
mock(ObjectProvider.class);
+
when(context.getBeanProvider(ClusterDataChangedEventForwarder.class)).thenReturn(forwarder);
+ DataChangedEventDispatcher dispatcher = new
DataChangedEventDispatcher(context);
+ ReflectionTestUtils.setField(dispatcher, "clusterProperties", new
ClusterProperties());
+ dispatcher.afterPropertiesSet();
+ doAnswer(invocation -> {
+ delivered.add("gateway");
+ return null;
+
}).when(gateway).onDiscoveryUpstreamChanged(org.mockito.ArgumentMatchers.anyList(),
any());
+ doAnswer(invocation -> {
+ ApplicationEvent event = invocation.getArgument(0);
+ if (event instanceof DataChangedEvent) {
+ dispatcher.onApplicationEvent((DataChangedEvent) event);
+ } else if (event instanceof DiscoveryStreamUpdatedEvent) {
+ delivered.add("health");
+ }
+ return null;
+ }).when(eventPublisher).publishEvent(any(ApplicationEvent.class));
+ }
+
+ @Test
+ void testDeletingLastRegistryUpstreamRefreshesEmptySnapshot() {
+ prepareRegistryChange();
+
+ discoveryDataChangedEventSyncListener.onChange(new
DiscoveryDataChangedEvent("key", "value",
DiscoveryDataChangedEvent.Event.DELETED));
+
+ ArgumentCaptor<DiscoveryStreamUpdatedEvent> captor =
ArgumentCaptor.forClass(DiscoveryStreamUpdatedEvent.class);
+ org.mockito.InOrder order = inOrder(eventPublisher);
+ order.verify(eventPublisher).publishEvent(captor.capture());
+ order.verify(eventPublisher).publishEvent(any(DataChangedEvent.class));
+
Assertions.assertTrue(captor.getValue().getDiscoverySyncData().getUpstreamDataList().isEmpty());
+ Assertions.assertEquals(EventTypeEnum.UPDATE,
captor.getValue().getType());
+ }
+
+ @Test
+ void testRegistryUpdateRefreshesHealthSnapshotWithoutTransaction() {
+ prepareRegistryChange();
+
+ discoveryDataChangedEventSyncListener.onChange(new
DiscoveryDataChangedEvent("key", "value",
DiscoveryDataChangedEvent.Event.UPDATED));
+
+
verify(eventPublisher).publishEvent(any(DiscoveryStreamUpdatedEvent.class));
+ }
+
+ private void prepareRegistryChange() {
+ DiscoveryUpstreamData upstream = new DiscoveryUpstreamData();
+ upstream.setUrl("127.0.0.1:8081");
+
when(keyValueParser.parseValue("value")).thenReturn(Collections.singletonList(upstream));
+
when(contextInfo.getNamespaceId()).thenReturn(SYS_DEFAULT_NAMESPACE_ID);
+ when(contextInfo.getPluginName()).thenReturn("divide");
+ when(contextInfo.getSelectorId()).thenReturn("selector-1");
+
when(contextInfo.getDiscoveryHandlerId()).thenReturn("discoveryHandlerId");
+ }
+
@Test
public void testOnChange() {
final List<DiscoveryUpstreamData> discoveryUpstreamDTOS = new
ArrayList<>();
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java
index 0cfd83239b..58628740da 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java
@@ -127,6 +127,14 @@ public final class DiscoveryUpstreamServiceTest {
);
}
+ @Test
+ void
testHasDiscoveryHandlerDistinguishesEmptyDiscoveryFromLegacyRegistration() {
+
when(discoveryHandlerMapper.selectBySelectorId("managed")).thenReturn(new
DiscoveryHandlerDO());
+
+
org.junit.jupiter.api.Assertions.assertTrue(discoveryUpstreamService.hasDiscoveryHandler("managed"));
+
org.junit.jupiter.api.Assertions.assertFalse(discoveryUpstreamService.hasDiscoveryHandler("legacy"));
+ }
+
@Test
public void testCreateOrUpdate() {
when(discoveryUpstreamMapper.insert(any())).thenReturn(1);
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamCheckServiceRecoveryTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamCheckServiceRecoveryTest.java
index 0b327d35cd..f0b773ee51 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamCheckServiceRecoveryTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamCheckServiceRecoveryTest.java
@@ -121,7 +121,8 @@ class UpstreamCheckServiceRecoveryTest {
ReflectionTestUtils.invokeMethod(service, "checkZombie",
ZombieUpstream.transform(upstream, 1, SELECTOR_ID));
} else {
service.replace(SELECTOR_ID, Collections.singletonList(upstream));
- ReflectionTestUtils.invokeMethod(service, "check", SELECTOR_ID,
Collections.singletonList(upstream));
+ Map<?, ?> upstreams = (Map<?, ?>)
ReflectionTestUtils.getField(UpstreamCheckService.class, "UPSTREAM_MAP");
+ ReflectionTestUtils.invokeMethod(service, "check", SELECTOR_ID,
upstreams.get(SELECTOR_ID));
}
CompletionException exception =
assertThrows(CompletionException.class, () ->
ReflectionTestUtils.invokeMethod(service, "waitFinish"));
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamCheckServiceSyncTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamCheckServiceSyncTest.java
index b653b25d18..a87294f456 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamCheckServiceSyncTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamCheckServiceSyncTest.java
@@ -23,33 +23,57 @@ import
org.apache.shenyu.admin.mapper.SelectorConditionMapper;
import org.apache.shenyu.admin.mapper.SelectorMapper;
import org.apache.shenyu.admin.model.entity.PluginDO;
import org.apache.shenyu.admin.model.entity.SelectorDO;
+import org.apache.shenyu.admin.model.enums.EventTypeEnum;
+import
org.apache.shenyu.admin.model.event.discovery.DiscoveryStreamUpdatedEvent;
import org.apache.shenyu.admin.service.converter.DivideSelectorHandleConverter;
import org.apache.shenyu.admin.service.converter.SelectorHandleConverterFactor;
import org.apache.shenyu.admin.service.impl.UpstreamCheckService;
import org.apache.shenyu.common.dto.DiscoverySyncData;
import org.apache.shenyu.common.dto.DiscoveryUpstreamData;
+import org.apache.shenyu.common.dto.convert.selector.CommonUpstream;
import org.apache.shenyu.common.dto.convert.selector.DivideUpstream;
import org.apache.shenyu.common.enums.ConfigGroupEnum;
import org.apache.shenyu.common.enums.PluginEnum;
import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.common.utils.UpstreamCheckUtils;
import org.apache.shenyu.register.common.config.ShenyuRegisterCenterConfig;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
+import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.util.ReflectionTestUtils;
+import java.sql.Timestamp;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
/**
@@ -81,14 +105,127 @@ class UpstreamCheckServiceSyncTest {
void setUp() {
SelectorHandleConverterFactor converters = new
SelectorHandleConverterFactor(
Collections.singletonMap(PluginEnum.DIVIDE.getName(), new
DivideSelectorHandleConverter()));
+ ShenyuRegisterCenterConfig config = new ShenyuRegisterCenterConfig();
+ config.setRegisterType("http");
+ config.getProps().setProperty("checked", "true");
service = new UpstreamCheckService(selectorMapper, eventPublisher,
pluginMapper, selectorConditionMapper,
- new ShenyuRegisterCenterConfig(), converters,
discoveryUpstreamService);
-
when(selectorMapper.selectById(SELECTOR_ID)).thenReturn(SelectorDO.builder().id(SELECTOR_ID)
+ config, converters, discoveryUpstreamService);
+
lenient().when(selectorMapper.selectById(SELECTOR_ID)).thenReturn(SelectorDO.builder().id(SELECTOR_ID)
.selectorName("/divide").namespaceId("namespace").pluginId("divide-id").handle("[]").build());
- when(pluginMapper.selectById("divide-id"))
+ lenient().when(pluginMapper.selectById("divide-id"))
.thenReturn(PluginDO.builder().id("divide-id").name(PluginEnum.DIVIDE.getName()).build());
}
+ @AfterEach
+ void tearDown() {
+ service.close();
+ UpstreamCheckService.removeByKey(SELECTOR_ID);
+ ((Collection<?>)
ReflectionTestUtils.getField(UpstreamCheckService.class,
"PENDING_SYNC")).clear();
+ ((Collection<?>)
ReflectionTestUtils.getField(UpstreamCheckService.class, "ZOMBIE_SET")).clear();
+ }
+
+ @Test
+ void testUncheckedHealthyDiscoveryUpstreamIsPreserved() {
+ DiscoveryUpstreamData offline =
DiscoveryUpstreamData.builder().url("127.0.0.1:8080").status(1).build();
+ DiscoveryUpstreamData replacement =
DiscoveryUpstreamData.builder().id("replacement")
+ .url("127.0.0.1:8081").protocol("http://").dateCreated(new
Timestamp(1)).status(0).weight(80).props("{}").build();
+
when(discoveryUpstreamService.findBySelectorId(SELECTOR_ID)).thenReturn(new
ArrayList<>(List.of(offline, replacement)));
+
+ ReflectionTestUtils.invokeMethod(service, "updateSelectorHandler",
SELECTOR_ID, Collections.emptyList(), Collections.emptySet());
+
+ DiscoverySyncData snapshot = publishedSnapshot();
+ assertEquals(1, snapshot.getUpstreamDataList().size());
+ assertEquals("replacement",
snapshot.getUpstreamDataList().get(0).getId());
+ assertEquals(80, snapshot.getUpstreamDataList().get(0).getWeight());
+ verify(discoveryUpstreamService,
never()).changeStatusBySelectorIdAndUrl(SELECTOR_ID, replacement.getUrl(),
Boolean.TRUE);
+ ArgumentCaptor<SelectorDO> selectorCaptor =
ArgumentCaptor.forClass(SelectorDO.class);
+ verify(selectorMapper).updateSelective(selectorCaptor.capture());
+
assertTrue(selectorCaptor.getValue().getHandle().contains(replacement.getUrl()));
+ }
+
+ @Test
+ void testPartiallyStaleResultPreservesUncheckedUpstream() {
+ DiscoveryUpstreamData checked =
DiscoveryUpstreamData.builder().url("127.0.0.1:8080").protocol("http://").status(0).build();
+ DiscoveryUpstreamData replacement =
DiscoveryUpstreamData.builder().url("127.0.0.1:8081").protocol("http://").dateCreated(new
Timestamp(1)).status(0).build();
+
when(discoveryUpstreamService.findBySelectorId(SELECTOR_ID)).thenReturn(new
ArrayList<>(List.of(checked, replacement)));
+ DivideUpstream alive =
DivideUpstream.builder().upstreamUrl(checked.getUrl()).protocol("http://").status(true).build();
+
+ ReflectionTestUtils.invokeMethod(service, "updateSelectorHandler",
SELECTOR_ID, Collections.singletonList(alive), Collections.emptySet());
+
+ assertEquals(2, publishedSnapshot().getUpstreamDataList().size());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testLateHealthCheckCannotOverwriteDiscoveryRefresh(final boolean
deleted) {
+ ScheduledThreadPoolExecutor executor =
mock(ScheduledThreadPoolExecutor.class);
+ List<Runnable> requests = new ArrayList<>();
+ doAnswer(invocation ->
requests.add(invocation.getArgument(0))).when(executor).execute(any(Runnable.class));
+ ReflectionTestUtils.setField(service, "invokeExecutor", executor);
+ CommonUpstream old =
DivideUpstream.builder().upstreamUrl("127.0.0.1:8080")
+
.protocol("http://").status(true).healthCheckEnabled(true).build();
+ service.replace(SELECTOR_ID, Collections.singletonList(old));
+ Map<?, ?> upstreams = (Map<?, ?>)
ReflectionTestUtils.getField(UpstreamCheckService.class, "UPSTREAM_MAP");
+ ReflectionTestUtils.invokeMethod(service, "check", SELECTOR_ID,
upstreams.get(SELECTOR_ID));
+ CommonUpstream replacement =
DivideUpstream.builder().upstreamUrl("127.0.0.1:8081")
+ .protocol("http://").status(true).build();
+ List<CommonUpstream> latest = deleted ? Collections.emptyList() :
Collections.singletonList(replacement);
+ service.replace(SELECTOR_ID, latest);
+
+ try (MockedStatic<UpstreamCheckUtils> checks =
mockStatic(UpstreamCheckUtils.class)) {
+ checks.when(() ->
UpstreamCheckUtils.checkUrl(old.getUpstreamUrl())).thenReturn(false);
+ requests.forEach(Runnable::run);
+ }
+ ReflectionTestUtils.invokeMethod(service, "waitFinish");
+
+ assertEquals(latest, upstreams.get(SELECTOR_ID));
+ verifyNoInteractions(eventPublisher);
+ }
+
+ @Test
+ void testAuthoritativeEmptySnapshotDoesNotResurrectCheckedUpstream() {
+
when(discoveryUpstreamService.hasDiscoveryHandler(SELECTOR_ID)).thenReturn(true);
+ DivideUpstream alive =
DivideUpstream.builder().upstreamUrl("127.0.0.1:8080").protocol("http://").status(true).build();
+
+ ReflectionTestUtils.invokeMethod(service, "updateSelectorHandler",
SELECTOR_ID, Collections.singletonList(alive), Collections.emptySet());
+
+ assertTrue(publishedSnapshot().getUpstreamDataList().isEmpty());
+ verify(discoveryUpstreamService,
never()).changeStatusBySelectorIdAndUrl(SELECTOR_ID, alive.getUpstreamUrl(),
Boolean.TRUE);
+ }
+
+ @Test
+ void testReplacementDiscoveredByAnotherAdminRemainsInHealthCache() {
+ DiscoveryUpstreamData replacement =
DiscoveryUpstreamData.builder().url("127.0.0.1:8081").protocol("http://")
+ .dateCreated(new Timestamp(1)).status(0).build();
+
when(discoveryUpstreamService.findBySelectorId(SELECTOR_ID)).thenReturn(Collections.singletonList(replacement));
+ CommonUpstream old =
DivideUpstream.builder().upstreamUrl("127.0.0.1:8080").protocol("http://").status(false).build();
+ service.replace(SELECTOR_ID, Collections.singletonList(old));
+
+ ReflectionTestUtils.invokeMethod(service, "updateHandler",
SELECTOR_ID, Collections.singletonList(old), Collections.emptyList());
+
+ assertEquals(replacement.getUrl(),
publishedSnapshot().getUpstreamDataList().get(0).getUrl());
+ @SuppressWarnings("unchecked")
+ Map<String, List<CommonUpstream>> upstreams = (Map<String,
List<CommonUpstream>>) ReflectionTestUtils.getField(UpstreamCheckService.class,
"UPSTREAM_MAP");
+ assertEquals(1, upstreams.get(SELECTOR_ID).size());
+ assertEquals(replacement.getUrl(),
upstreams.get(SELECTOR_ID).get(0).getUpstreamUrl());
+ }
+
+ @Test
+ void testFailedCheckOverridesPersistedHealthyStatus() {
+ DiscoveryUpstreamData failed =
DiscoveryUpstreamData.builder().url("127.0.0.1:8080").protocol("http://").status(0).build();
+ DiscoveryUpstreamData replacement =
DiscoveryUpstreamData.builder().url("127.0.0.1:8081").protocol("http://")
+ .dateCreated(new Timestamp(1)).status(0).build();
+
when(discoveryUpstreamService.findBySelectorId(SELECTOR_ID)).thenReturn(List.of(failed,
replacement));
+ CommonUpstream old =
DivideUpstream.builder().upstreamUrl(failed.getUrl()).protocol("http://").status(false).build();
+ service.replace(SELECTOR_ID, Collections.singletonList(old));
+
+ ReflectionTestUtils.invokeMethod(service, "updateHandler",
SELECTOR_ID, Collections.singletonList(old), Collections.emptyList());
+
+ List<DiscoveryUpstreamData> outgoing =
publishedSnapshot().getUpstreamDataList();
+ assertEquals(1, outgoing.size());
+ assertEquals(replacement.getUrl(), outgoing.get(0).getUrl());
+ }
+
@Test
void testRecoveredUpstreamPublishesCurrentStatus() {
DiscoveryUpstreamData recovered =
DiscoveryUpstreamData.builder().id("recovered")
@@ -97,7 +234,7 @@ class UpstreamCheckServiceSyncTest {
when(discoveryUpstreamService.findBySelectorId(SELECTOR_ID)).thenReturn(new
ArrayList<>(List.of(recovered, offline)));
DivideUpstream alive =
DivideUpstream.builder().upstreamUrl(recovered.getUrl()).protocol("http://").status(true).build();
- ReflectionTestUtils.invokeMethod(service, "updateSelectorHandler",
SELECTOR_ID, Collections.singletonList(alive));
+ ReflectionTestUtils.invokeMethod(service, "updateSelectorHandler",
SELECTOR_ID, Collections.singletonList(alive), Collections.emptySet());
verify(discoveryUpstreamService).changeStatusBySelectorIdAndUrl(SELECTOR_ID,
recovered.getUrl(), Boolean.TRUE);
DiscoverySyncData snapshot = publishedSnapshot();
@@ -114,7 +251,7 @@ class UpstreamCheckServiceSyncTest {
DiscoveryUpstreamData offline =
DiscoveryUpstreamData.builder().url("127.0.0.1:8080").status(1).build();
when(discoveryUpstreamService.findBySelectorId(SELECTOR_ID)).thenReturn(new
ArrayList<>(Collections.singletonList(offline)));
- ReflectionTestUtils.invokeMethod(service, "updateSelectorHandler",
SELECTOR_ID, Collections.emptyList());
+ ReflectionTestUtils.invokeMethod(service, "updateSelectorHandler",
SELECTOR_ID, Collections.emptyList(), Collections.emptySet());
assertTrue(publishedSnapshot().getUpstreamDataList().isEmpty());
}
@@ -123,7 +260,7 @@ class UpstreamCheckServiceSyncTest {
void testUpstreamWithoutDiscoveryRecordPublishesHealthyStatus() {
DivideUpstream alive =
DivideUpstream.builder().upstreamUrl("127.0.0.1:8080").protocol("http://").status(true).build();
- ReflectionTestUtils.invokeMethod(service, "updateSelectorHandler",
SELECTOR_ID, Collections.singletonList(alive));
+ ReflectionTestUtils.invokeMethod(service, "updateSelectorHandler",
SELECTOR_ID, Collections.singletonList(alive), Collections.emptySet());
DiscoverySyncData snapshot = publishedSnapshot();
assertEquals(1, snapshot.getUpstreamDataList().size());
@@ -131,6 +268,114 @@ class UpstreamCheckServiceSyncTest {
assertEquals(0, snapshot.getUpstreamDataList().get(0).getStatus());
}
+ @Test
+ void testRegistrationCanProceedDuringSlowEventDispatch() throws Exception {
+ CommonUpstream old =
DivideUpstream.builder().upstreamUrl("127.0.0.1:8080").protocol("http://").status(false).build();
+ CommonUpstream added =
DivideUpstream.builder().upstreamUrl("127.0.0.1:8081").protocol("http://").status(true).build();
+ service.replace(SELECTOR_ID, Collections.singletonList(old));
+ CountDownLatch publishing = new CountDownLatch(1);
+ CountDownLatch release = new CountDownLatch(1);
+ doAnswer(invocation -> {
+ DataChangedEvent event = invocation.getArgument(0);
+ if (event.getGroupKey() == ConfigGroupEnum.SELECTOR) {
+ publishing.countDown();
+ assertTrue(release.await(5, TimeUnit.SECONDS));
+ }
+ return null;
+ }).when(eventPublisher).publishEvent(any(DataChangedEvent.class));
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+ try {
+ final Future<?> check = executor.submit(() ->
ReflectionTestUtils.invokeMethod(service, "updateHandler", SELECTOR_ID,
+ Collections.singletonList(old), Collections.emptyList()));
+ assertTrue(publishing.await(5, TimeUnit.SECONDS));
+ Future<?> registration = executor.submit(() ->
ReflectionTestUtils.invokeMethod(service, "submitJust", SELECTOR_ID, added));
+ registration.get(2, TimeUnit.SECONDS);
+ release.countDown();
+ check.get(5, TimeUnit.SECONDS);
+ Map<?, ?> upstreams = (Map<?, ?>)
ReflectionTestUtils.getField(UpstreamCheckService.class, "UPSTREAM_MAP");
+ assertTrue(upstreams.containsKey(SELECTOR_ID));
+ assertTrue(((List<?>) upstreams.get(SELECTOR_ID)).contains(added));
+ } finally {
+ release.countDown();
+ executor.shutdownNow();
+ assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
+ }
+ }
+
+ @Test
+ void testQueuedRegistrationCannotPublishSupersededList() {
+ ScheduledThreadPoolExecutor executor =
mock(ScheduledThreadPoolExecutor.class);
+ List<Runnable> updates = new ArrayList<>();
+ doAnswer(invocation ->
updates.add(invocation.getArgument(0))).when(executor).execute(any(Runnable.class));
+ ReflectionTestUtils.setField(service, "executor", executor);
+ CommonUpstream first =
DivideUpstream.builder().upstreamUrl("127.0.0.1:8080").protocol("http://").status(true).build();
+ CommonUpstream second =
DivideUpstream.builder().upstreamUrl("127.0.0.1:8081").protocol("http://").status(true).build();
+ service.submit(SELECTOR_ID, first);
+ service.submit(SELECTOR_ID, second);
+
+ updates.get(0).run();
+ verifyNoInteractions(eventPublisher);
+ updates.get(1).run();
+
+ assertEquals(2, publishedSnapshot().getUpstreamDataList().size());
+ }
+
+ @Test
+ void testReentrantDiscoveryRefreshDuringPublicationIsRetained() {
+ CommonUpstream old =
DivideUpstream.builder().upstreamUrl("127.0.0.1:8080").protocol("http://").status(false).build();
+ CommonUpstream replacement =
DivideUpstream.builder().upstreamUrl("127.0.0.1:8081").protocol("http://").status(true).build();
+ service.replace(SELECTOR_ID, Collections.singletonList(old));
+ doAnswer(invocation -> {
+ DataChangedEvent event = invocation.getArgument(0);
+ if (event.getGroupKey() == ConfigGroupEnum.DISCOVER_UPSTREAM) {
+ service.replace(SELECTOR_ID,
Collections.singletonList(replacement));
+ }
+ return null;
+ }).when(eventPublisher).publishEvent(any(DataChangedEvent.class));
+
+ ReflectionTestUtils.invokeMethod(service, "updateHandler",
SELECTOR_ID, Collections.singletonList(old), Collections.emptyList());
+
+ Map<?, ?> upstreams = (Map<?, ?>)
ReflectionTestUtils.getField(UpstreamCheckService.class, "UPSTREAM_MAP");
+ assertEquals(Collections.singletonList(replacement),
upstreams.get(SELECTOR_ID));
+ }
+
+ @Test
+ void testMixedHttpRegistrationDoesNotOverrideAuthoritativeDiscovery() {
+ CommonUpstream http =
DivideUpstream.builder().upstreamUrl("127.0.0.1:8080").protocol("http://").status(true).build();
+ DiscoveryUpstreamData discovered =
DiscoveryUpstreamData.builder().url("127.0.0.1:8081").protocol("http://")
+ .dateCreated(new Timestamp(1)).status(0).build();
+
when(discoveryUpstreamService.findBySelectorId(SELECTOR_ID)).thenReturn(Collections.singletonList(discovered));
+
+ ReflectionTestUtils.invokeMethod(service, "updateSelectorHandler",
SELECTOR_ID, Collections.singletonList(http), Collections.emptySet());
+
+ List<DiscoveryUpstreamData> outgoing =
publishedSnapshot().getUpstreamDataList();
+ assertEquals(1, outgoing.size());
+ assertEquals(discovered.getUrl(), outgoing.get(0).getUrl());
+ ArgumentCaptor<SelectorDO> selector =
ArgumentCaptor.forClass(SelectorDO.class);
+ verify(selectorMapper).updateSelective(selector.capture());
+
assertTrue(selector.getValue().getHandle().contains(discovered.getUrl()));
+
org.junit.jupiter.api.Assertions.assertFalse(selector.getValue().getHandle().contains(http.getUpstreamUrl()));
+ }
+
+ @Test
+ void testDiscoveryRefreshPreservesPersistedHealthState() {
+ DiscoveryUpstreamData healthy =
DiscoveryUpstreamData.builder().url("127.0.0.1:8080").protocol("http://")
+ .dateCreated(new Timestamp(1)).status(0).build();
+ DiscoveryUpstreamData unhealthy =
DiscoveryUpstreamData.builder().url("127.0.0.1:8081").protocol("http://")
+ .dateCreated(new Timestamp(1)).status(1).build();
+ DiscoverySyncData snapshot = new DiscoverySyncData();
+ snapshot.setSelectorId(SELECTOR_ID);
+ snapshot.setPluginName(PluginEnum.DIVIDE.getName());
+ snapshot.setUpstreamDataList(List.of(healthy, unhealthy));
+
+ service.onDiscoveryUpstreamUpdated(new
DiscoveryStreamUpdatedEvent(snapshot, EventTypeEnum.UPDATE));
+
+ @SuppressWarnings("unchecked")
+ Map<String, List<CommonUpstream>> upstreams = (Map<String,
List<CommonUpstream>>) ReflectionTestUtils.getField(UpstreamCheckService.class,
"UPSTREAM_MAP");
+ assertTrue(upstreams.get(SELECTOR_ID).get(0).isStatus());
+
org.junit.jupiter.api.Assertions.assertFalse(upstreams.get(SELECTOR_ID).get(1).isStatus());
+ }
+
private DiscoverySyncData publishedSnapshot() {
ArgumentCaptor<DataChangedEvent> captor =
ArgumentCaptor.forClass(DataChangedEvent.class);
verify(eventPublisher, times(2)).publishEvent(captor.capture());