This is an automated email from the ASF dual-hosted git repository.
funky-eyes pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/incubator-seata.git
The following commit(s) were added to refs/heads/2.x by this push:
new 0b0101b7c9 optimize: supports switching http protocols based on server
versions in RaftRegistryService (#7952)
0b0101b7c9 is described below
commit 0b0101b7c9b46b17694e7863e684f393b01d090e
Author: Tsukilc <[email protected]>
AuthorDate: Wed Apr 29 14:26:00 2026 +0800
optimize: supports switching http protocols based on server versions in
RaftRegistryService (#7952)
---
changes/en-us/2.x.md | 1 +
changes/zh-cn/2.x.md | 1 +
discovery/seata-discovery-raft/pom.xml | 7 +-
.../registry/raft/RaftRegistryServiceImpl.java | 421 ++++++++++++---
.../registry/raft/RaftRegistryServiceImplTest.java | 564 ++++++++++++++++++---
5 files changed, 862 insertions(+), 132 deletions(-)
diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index 0fc495b560..db0376a775 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -32,6 +32,7 @@ Add changes here for all PR submitted to the 2.x branch.
- [[#8020](https://github.com/apache/incubator-seata/pull/8020)] add
UnregisterRM protocol to notify server on client destroy
- [[#8044](https://github.com/apache/incubator-seata/pull/8044)] add protobuf
serialization support for UnregisterRM protocol
- [[#8046](https://github.com/apache/incubator-seata/pull/8046)] add fastjson2
and jackson3
+- [[#7952](https://github.com/apache/incubator-seata/pull/7952)] Support
runtime dynamic switching between HTTP/1.1 and HTTP/2 for Raft client watch,
with a 2.7.0 version threshold and compatibility fallback
### bugfix:
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index c7140a9767..581d7b7d5b 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -33,6 +33,7 @@
- [[#8020](https://github.com/apache/incubator-seata/pull/8020)] 新增
UnregisterRM 协议,在客户端销毁时通知服务端
- [[#8044](https://github.com/apache/incubator-seata/pull/8044)] 为
UnregisterRM 协议添加 protobuf 序列化支持
- [[#8046](https://github.com/apache/incubator-seata/pull/8046)] 添加了 fastjson2
和 jackson3
+- [[#7952](https://github.com/apache/incubator-seata/pull/7952)]
在Raft客户端支持Watch在HTTP/1.1与HTTP/2之间运行时动态切换
### bugfix:
diff --git a/discovery/seata-discovery-raft/pom.xml
b/discovery/seata-discovery-raft/pom.xml
index a4f3a7a209..fb606cde2d 100644
--- a/discovery/seata-discovery-raft/pom.xml
+++ b/discovery/seata-discovery-raft/pom.xml
@@ -35,6 +35,11 @@
<artifactId>seata-discovery-core</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.seata</groupId>
+ <artifactId>seata-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
@@ -48,4 +53,4 @@
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
-</project>
\ No newline at end of file
+</project>
diff --git
a/discovery/seata-discovery-raft/src/main/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImpl.java
b/discovery/seata-discovery-raft/src/main/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImpl.java
index aa154aed4d..942f34701b 100644
---
a/discovery/seata-discovery-raft/src/main/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImpl.java
+++
b/discovery/seata-discovery-raft/src/main/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImpl.java
@@ -28,6 +28,7 @@ import
org.apache.seata.common.exception.AuthenticationFailedException;
import org.apache.seata.common.exception.NotSupportYetException;
import org.apache.seata.common.exception.ParseEndpointException;
import org.apache.seata.common.exception.RetryableException;
+import org.apache.seata.common.metadata.ClusterWatchEvent;
import org.apache.seata.common.metadata.Metadata;
import org.apache.seata.common.metadata.MetadataResponse;
import org.apache.seata.common.metadata.Node;
@@ -35,10 +36,12 @@ import org.apache.seata.common.thread.NamedThreadFactory;
import org.apache.seata.common.util.CollectionUtils;
import org.apache.seata.common.util.HttpClientUtil;
import org.apache.seata.common.util.NetUtil;
+import org.apache.seata.common.util.SeataHttpWatch;
import org.apache.seata.common.util.StringUtils;
import org.apache.seata.config.ConfigChangeListener;
import org.apache.seata.config.Configuration;
import org.apache.seata.config.ConfigurationFactory;
+import org.apache.seata.core.protocol.Version;
import org.apache.seata.discovery.registry.RegistryService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -49,10 +52,12 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadLocalRandom;
@@ -64,7 +69,6 @@ import java.util.stream.Stream;
/**
* The type File registry service.
- *
*/
public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeListener> {
@@ -114,6 +118,22 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
private static final AtomicBoolean CLOSED = new AtomicBoolean(false);
+ private static final long DEFAULT_METADATA_MAX_AGE_MS = 30000L;
+
+ private static final long WATCH_TIMEOUT_MS = 30000L;
+
+ private static final long RETRY_DELAY_MS = 1000L;
+
+ private static final int HTTP2_WATCH_READ_TIMEOUT_SECONDS = 30;
+
+ private static final String MIN_HTTP2_VERSION = "2.7.0";
+
+ private static volatile WatchProtocol CURRENT_WATCH_PROTOCOL =
WatchProtocol.HTTP1;
+
+ private static volatile SeataHttpWatch<ClusterWatchEvent> HTTP2_WATCH;
+
+ private static volatile String HTTP2_WATCH_GROUP;
+
/**
* Service node health check
*/
@@ -121,6 +141,18 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
private static final String PREFERRED_NETWORKS;
+ /**
+ * Protocol used by watch mechanism.
+ */
+ private enum WatchProtocol {
+
+ /** HTTP/1.x protocol */
+ HTTP1,
+
+ /** HTTP/2 protocol */
+ HTTP2
+ }
+
static {
TOKEN_EXPIRE_TIME_IN_MILLISECONDS =
CONFIG.getLong(getTokenExpireTimeInMillisecondsKey(), 29 * 60 * 1000L);
USERNAME = CONFIG.getConfig(getRaftUserNameKey());
@@ -146,6 +178,7 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
return instance;
}
+ @SuppressWarnings("AliDeprecation")
@Override
public void register(InetSocketAddress address) throws Exception {}
@@ -170,23 +203,20 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
new LinkedBlockingQueue<>(),
new NamedThreadFactory("refreshMetadata", 1,
true));
REFRESH_METADATA_EXECUTOR.execute(() -> {
- long metadataMaxAgeMs =
CONFIG.getLong(getMetadataMaxAgeMs(), 30000L);
+ long metadataMaxAgeMs =
CONFIG.getLong(getMetadataMaxAgeMs(), DEFAULT_METADATA_MAX_AGE_MS);
long currentTime = System.currentTimeMillis();
while (!CLOSED.get()) {
try {
- // Forced refresh of metadata information
after set age
boolean fetch = System.currentTimeMillis() -
currentTime > metadataMaxAgeMs;
String clusterName =
CURRENT_TRANSACTION_CLUSTER_NAME;
if (!fetch) {
fetch = watch();
}
- // Cluster changes or reaches timeout refresh
time
if (fetch) {
for (String group :
METADATA.groups(clusterName)) {
try {
acquireClusterMetaData(clusterName, group);
} catch (Exception e) {
- // prevents an exception from
being thrown that causes the thread to break
if (e instanceof
RetryableException) {
throw e;
} else {
@@ -203,21 +233,305 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
} catch (RetryableException e) {
LOGGER.error(e.getMessage(), e);
try {
- Thread.sleep(1000);
+ Thread.sleep(RETRY_DELAY_MS);
} catch (InterruptedException ignored) {
}
}
}
+ closeHttp2Watch();
});
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
CLOSED.compareAndSet(false, true);
- REFRESH_METADATA_EXECUTOR.shutdown();
+ closeHttp2Watch();
+ if (REFRESH_METADATA_EXECUTOR != null) {
+ REFRESH_METADATA_EXECUTOR.shutdown();
+ }
}));
}
}
}
}
+ private static boolean watch() throws RetryableException {
+ String clusterName = CURRENT_TRANSACTION_CLUSTER_NAME;
+ if (StringUtils.isBlank(clusterName)) {
+ return false;
+ }
+
+ WatchProtocol targetProtocol = resolveWatchProtocol(clusterName);
+ switchWatchProtocolIfNecessary(targetProtocol);
+
+ if (targetProtocol == WatchProtocol.HTTP2) {
+ return watchHttp2(clusterName);
+ }
+ return watchHttp1(clusterName);
+ }
+
+ private static void switchWatchProtocolIfNecessary(WatchProtocol
targetProtocol) {
+ if (CURRENT_WATCH_PROTOCOL == targetProtocol) {
+ return;
+ }
+
+ LOGGER.info("Switching raft watch protocol from {} to {}",
CURRENT_WATCH_PROTOCOL, targetProtocol);
+ if (targetProtocol == WatchProtocol.HTTP1) {
+ closeHttp2Watch();
+ }
+ CURRENT_WATCH_PROTOCOL = targetProtocol;
+ }
+
+ private static WatchProtocol resolveWatchProtocol(String clusterName) {
+ if (StringUtils.isBlank(clusterName)) {
+ return WatchProtocol.HTTP1;
+ }
+
+ Set<String> groups = METADATA.groups(clusterName);
+ if (CollectionUtils.isEmpty(groups)) {
+ return WatchProtocol.HTTP1;
+ }
+
+ boolean hasNode = false;
+ for (String group : groups) {
+ List<Node> nodes = METADATA.getNodes(clusterName, group);
+ if (CollectionUtils.isEmpty(nodes)) {
+ continue;
+ }
+ hasNode = true;
+ if (!isClusterHttp2Enabled(clusterName, group)) {
+ return WatchProtocol.HTTP1;
+ }
+ }
+
+ return hasNode ? WatchProtocol.HTTP2 : WatchProtocol.HTTP1;
+ }
+
+ private static boolean watchHttp1(String clusterName) throws
RetryableException {
+ Map<String, String> header = new HashMap<>();
+ header.put(HTTP.CONTENT_TYPE,
ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
+ Map<String, String> param = new HashMap<>();
+ Map<String, Long> groupTerms = METADATA.getClusterTerm(clusterName);
+ groupTerms.forEach((k, v) -> param.put(k, String.valueOf(v)));
+ for (String group : groupTerms.keySet()) {
+ String tcAddress = queryHttpAddress(clusterName, group);
+ if (StringUtils.isBlank(tcAddress)) {
+ return false;
+ }
+ if (isTokenExpired()) {
+ refreshToken(tcAddress);
+ }
+ if (StringUtils.isNotBlank(jwtToken)) {
+ header.put(AUTHORIZATION_HEADER, jwtToken);
+ }
+ try (Response response = HttpClientUtil.doPost(
+ "http://" + tcAddress + "/metadata/v1/watch", param,
header, (int) WATCH_TIMEOUT_MS)) {
+ if (response != null) {
+ int statusCode = response.code();
+ if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
+ if (StringUtils.isNotBlank(USERNAME) &&
StringUtils.isNotBlank(PASSWORD)) {
+ throw new RetryableException("Authentication
failed!");
+ } else {
+ throw new AuthenticationFailedException(
+ "Authentication failed! you should
configure the correct username and password.");
+ }
+ }
+ return statusCode == HttpStatus.SC_OK;
+ }
+ } catch (IOException e) {
+ LOGGER.error("watch cluster node: {}, fail: {}", tcAddress,
e.getMessage());
+ throw new RetryableException(e.getMessage(), e);
+ }
+ break;
+ }
+ return false;
+ }
+
+ private static boolean watchHttp2(String clusterName) throws
RetryableException {
+ Map<String, Long> groupTerms = METADATA.getClusterTerm(clusterName);
+ if (CollectionUtils.isEmpty(groupTerms)) {
+ return false;
+ }
+
+ String group = selectWatchGroup(groupTerms);
+ if (StringUtils.isBlank(group)) {
+ return false;
+ }
+ String tcAddress = queryHttpAddress(clusterName, group);
+ if (StringUtils.isBlank(tcAddress)) {
+ return false;
+ }
+
+ Map<String, String> header = new HashMap<>();
+ header.put(HTTP.CONTENT_TYPE,
ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
+
+ Map<String, String> param = new HashMap<>();
+ groupTerms.forEach((k, v) -> param.put(k, String.valueOf(v)));
+
+ if (isTokenExpired()) {
+ refreshToken(tcAddress);
+ }
+ if (StringUtils.isNotBlank(jwtToken)) {
+ header.put(AUTHORIZATION_HEADER, jwtToken);
+ }
+
+ ensureHttp2Watch(group, tcAddress, param, header);
+ SeataHttpWatch<ClusterWatchEvent> watch = HTTP2_WATCH;
+ if (watch == null) {
+ return false;
+ }
+
+ try {
+ SeataHttpWatch.Response<ClusterWatchEvent> response = watch.next();
+ return shouldRefreshMetadata(clusterName, group, response);
+ } catch (RuntimeException e) {
+ if (CLOSED.get()) {
+ closeHttp2Watch();
+ return false;
+ }
+ closeHttp2Watch();
+ throw new RetryableException("HTTP2 watch failed", e);
+ }
+ }
+
+ private static String selectWatchGroup(Map<String, Long> groupTerms) {
+ if (CollectionUtils.isEmpty(groupTerms)) {
+ return null;
+ }
+
+ if (StringUtils.isNotBlank(HTTP2_WATCH_GROUP) &&
groupTerms.containsKey(HTTP2_WATCH_GROUP)) {
+ return HTTP2_WATCH_GROUP;
+ }
+
+ List<String> groups = new ArrayList<>(groupTerms.keySet());
+ Collections.sort(groups);
+ return groups.get(0);
+ }
+
+ private static synchronized void ensureHttp2Watch(
+ String group, String tcAddress, Map<String, String> param,
Map<String, String> header)
+ throws RetryableException {
+
+ if (HTTP2_WATCH != null && StringUtils.equals(group,
HTTP2_WATCH_GROUP)) {
+ return;
+ }
+
+ closeHttp2Watch();
+
+ try {
+ HTTP2_WATCH = HttpClientUtil.watchPost(
+ "http://" + tcAddress + "/metadata/v1/watch",
+ param,
+ header,
+ ClusterWatchEvent.class,
+ HTTP2_WATCH_READ_TIMEOUT_SECONDS);
+ HTTP2_WATCH_GROUP = group;
+ } catch (IOException e) {
+ closeHttp2Watch();
+ throw new RetryableException(e.getMessage(), e);
+ } catch (RuntimeException e) {
+ closeHttp2Watch();
+ if (e.getMessage() != null && e.getMessage().contains("401")) {
+ tokenTimeStamp = -1;
+ }
+ throw new RetryableException("Failed to create HTTP2 watch", e);
+ }
+ }
+
+ private static boolean shouldRefreshMetadata(
+ String clusterName, String defaultGroup,
SeataHttpWatch.Response<ClusterWatchEvent> response) {
+
+ if (response == null
+ || response.type != SeataHttpWatch.Response.Type.UPDATE
+ || response.object == null
+ || response.object.getMetadata() == null
+ ||
CollectionUtils.isEmpty(response.object.getMetadata().getNodes())) {
+ return false;
+ }
+
+ ClusterWatchEvent event = response.object;
+ MetadataResponse incomingMetadata = event.getMetadata();
+
+ String eventGroup = StringUtils.isNotBlank(event.getGroup()) ?
event.getGroup() : defaultGroup;
+ long localTerm =
METADATA.getClusterTerm(clusterName).getOrDefault(eventGroup, -1L);
+ if (incomingMetadata.getTerm() < localTerm) {
+ return false;
+ }
+ boolean termAdvanced = incomingMetadata.getTerm() > localTerm;
+
+ boolean changed = termAdvanced || hasMetadataChanged(clusterName,
eventGroup, incomingMetadata);
+
+ if (changed) {
+ METADATA.refreshMetadata(clusterName, incomingMetadata);
+ }
+
+ return changed;
+ }
+
+ private static boolean hasMetadataChanged(String clusterName, String
group, MetadataResponse incomingMetadata) {
+ if (incomingMetadata == null) {
+ return false;
+ }
+
+ List<Node> incomingNodes = incomingMetadata.getNodes();
+ List<Node> localNodes = METADATA.getNodes(clusterName, group);
+
+ if (CollectionUtils.isEmpty(localNodes) !=
CollectionUtils.isEmpty(incomingNodes)) {
+ return true;
+ }
+
+ if (CollectionUtils.isEmpty(localNodes)) {
+ return false;
+ }
+
+ if (incomingMetadata.getTerm() >
METADATA.getClusterTerm(clusterName).getOrDefault(group, -1L)) {
+ return true;
+ }
+
+ if (localNodes.size() != incomingNodes.size()) {
+ return true;
+ }
+
+ return
!buildNodeSignatures(localNodes).equals(buildNodeSignatures(incomingNodes));
+ }
+
+ private static Set<String> buildNodeSignatures(List<Node> nodes) {
+ Set<String> signatures = new HashSet<>();
+ for (Node node : nodes) {
+ signatures.add(buildNodeSignature(node));
+ }
+ return signatures;
+ }
+
+ private static String buildNodeSignature(Node node) {
+ if (node == null) {
+ return "";
+ }
+
+ String control = node.getControl() == null
+ ? ""
+ : node.getControl().getHost()
+ + IP_PORT_SPLIT_CHAR
+ + node.getControl().getPort();
+ String transaction = node.getTransaction() == null
+ ? ""
+ : node.getTransaction().getHost()
+ + IP_PORT_SPLIT_CHAR
+ + node.getTransaction().getPort();
+
+ return control + "|" + transaction + "|" + node.getRole() + "|" +
node.getVersion() + "|" + node.getGroup();
+ }
+
+ private static synchronized void closeHttp2Watch() {
+ SeataHttpWatch<ClusterWatchEvent> watch = HTTP2_WATCH;
+ HTTP2_WATCH = null;
+ HTTP2_WATCH_GROUP = null;
+ if (watch != null) {
+ try {
+ watch.close();
+ } catch (IOException e) {
+ LOGGER.warn("Failed to close HTTP2 watch stream", e);
+ }
+ }
+ }
+
private static String queryHttpAddress(String clusterName, String group) {
List<Node> nodeList = METADATA.getNodes(clusterName, group);
List<String> addressList = null;
@@ -232,7 +546,11 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
stream = inetSocketAddresses.stream();
}
} else {
- stream = INIT_ADDRESSES.get(clusterName).stream();
+ List<InetSocketAddress> initAddresses =
INIT_ADDRESSES.get(clusterName);
+ if (CollectionUtils.isEmpty(initAddresses)) {
+ return null;
+ }
+ stream = initAddresses.stream();
}
if (addressList != null) {
return
addressList.get(ThreadLocalRandom.current().nextInt(addressList.size()));
@@ -256,7 +574,9 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
+ (controlEndpoint != null ?
controlEndpoint.getPort() : inetSocketAddress.getPort());
})
.collect(Collectors.toList());
- return
addressList.get(ThreadLocalRandom.current().nextInt(addressList.size()));
+ return addressList.isEmpty()
+ ? null
+ :
addressList.get(ThreadLocalRandom.current().nextInt(addressList.size()));
}
}
@@ -325,7 +645,6 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
private static InetSocketAddress selectEndpoint(String type, Node node) {
if (StringUtils.isBlank(PREFERRED_NETWORKS)) {
- // Use the default method, directly using node.control and
node.transaction
switch (type) {
case "control":
return new InetSocketAddress(
@@ -393,6 +712,7 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
@Override
public void close() {
CLOSED.compareAndSet(false, true);
+ closeHttp2Watch();
}
@Override
@@ -407,44 +727,6 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
return RegistryService.super.aliveLookup(transactionServiceGroup);
}
- private static boolean watch() throws RetryableException {
- Map<String, String> header = new HashMap<>();
- header.put(HTTP.CONTENT_TYPE,
ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
- Map<String, String> param = new HashMap<>();
- String clusterName = CURRENT_TRANSACTION_CLUSTER_NAME;
- Map<String, Long> groupTerms = METADATA.getClusterTerm(clusterName);
- groupTerms.forEach((k, v) -> param.put(k, String.valueOf(v)));
- for (String group : groupTerms.keySet()) {
- String tcAddress = queryHttpAddress(clusterName, group);
- if (isTokenExpired()) {
- refreshToken(tcAddress);
- }
- if (StringUtils.isNotBlank(jwtToken)) {
- header.put(AUTHORIZATION_HEADER, jwtToken);
- }
- try (Response response =
- HttpClientUtil.doPost("http://" + tcAddress +
"/metadata/v1/watch", param, header, 30000)) {
- if (response != null) {
- int statusCode = response.code();
- if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
- if (StringUtils.isNotBlank(USERNAME) &&
StringUtils.isNotBlank(PASSWORD)) {
- throw new RetryableException("Authentication
failed!");
- } else {
- throw new AuthenticationFailedException(
- "Authentication failed! you should
configure the correct username and password.");
- }
- }
- return statusCode == HttpStatus.SC_OK;
- }
- } catch (IOException e) {
- LOGGER.error("watch cluster node: {}, fail: {}", tcAddress,
e.getMessage());
- throw new RetryableException(e.getMessage(), e);
- }
- break;
- }
- return false;
- }
-
@Override
public List<InetSocketAddress> refreshAliveLookup(
String transactionServiceGroup, List<InetSocketAddress>
aliveAddress) {
@@ -457,8 +739,6 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
? aliveAddress
: aliveAddress.parallelStream()
.filter(inetSocketAddress -> {
- // Since only follower will turn into
leader, only the follower node needs to be
- // listened to
return inetSocketAddress.getPort() !=
leaderAddress.getPort()
|| !inetSocketAddress
.getAddress()
@@ -518,10 +798,17 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
"Authentication failed! you should configure
the correct username and password.");
}
}
- MetadataResponse metadataResponse;
if (StringUtils.isNotBlank(response)) {
try {
- metadataResponse = OBJECT_MAPPER.readValue(response,
MetadataResponse.class);
+ MetadataResponse metadataResponse =
OBJECT_MAPPER.readValue(response, MetadataResponse.class);
+ if
(CollectionUtils.isEmpty(metadataResponse.getNodes())) {
+ LOGGER.warn(
+ "empty metadata nodes from cluster
endpoint, clusterName={}, group={}, response={}",
+ clusterName,
+ group,
+ response);
+ return;
+ }
METADATA.refreshMetadata(clusterName,
metadataResponse);
} catch (JsonProcessingException e) {
LOGGER.error(e.getMessage(), e);
@@ -534,11 +821,9 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
}
private static void refreshToken(String tcAddress) throws
RetryableException {
- // if username and password is not in config , return
if (StringUtils.isBlank(USERNAME) || StringUtils.isBlank(PASSWORD)) {
return;
}
- // get token and set it in cache
Map<String, String> param = new HashMap<>();
param.put(PRO_USERNAME_KEY, USERNAME);
param.put(PRO_PASSWORD_KEY, PASSWORD);
@@ -554,7 +839,6 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
JsonNode jsonNode = OBJECT_MAPPER.readTree(response);
String codeStatus = jsonNode.get("code").asText();
if (!StringUtils.equals(codeStatus, "200")) {
- // authorized failed,throw exception to kill
process
throw new AuthenticationFailedException(
"Authentication failed! you should
configure the correct username and password.");
}
@@ -564,7 +848,6 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
throw new
AuthenticationFailedException("Authentication failed! Response body is null.");
}
} else {
- // authorized failed,throw exception to kill process
throw new AuthenticationFailedException(
"Authentication failed! you should configure the
correct username and password.");
}
@@ -574,6 +857,30 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
}
}
+ private static boolean supportsHttp2(Node node) {
+ if (node == null) {
+ return false;
+ }
+ String version = node.getVersion();
+ if (StringUtils.isBlank(version)) {
+ return false;
+ }
+ try {
+ return Version.isAboveOrEqualVersion(version, MIN_HTTP2_VERSION);
+ } catch (Exception e) {
+ LOGGER.warn("Invalid version: {}, fallback to HTTP/1.1", version);
+ return false;
+ }
+ }
+
+ private static boolean isClusterHttp2Enabled(String clusterName, String
group) {
+ List<Node> nodes = METADATA.getNodes(clusterName, group);
+ if (CollectionUtils.isEmpty(nodes)) {
+ return false;
+ }
+ return nodes.stream().allMatch(RaftRegistryServiceImpl::supportsHttp2);
+ }
+
@Override
public List<InetSocketAddress> lookup(String key) throws Exception {
String clusterName = getServiceGroup(key);
@@ -597,13 +904,11 @@ public class RaftRegistryServiceImpl implements
RegistryService<ConfigChangeList
return null;
}
INIT_ADDRESSES.put(clusterName, list);
- // init jwt token
try {
refreshToken(queryHttpAddress(clusterName, key));
} catch (Exception e) {
throw new RuntimeException("Init fetch token failed!", e);
}
- // Refresh the metadata by initializing the address
acquireClusterMetaDataByClusterName(clusterName);
startQueryMetadata();
}
diff --git
a/discovery/seata-discovery-raft/src/test/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImplTest.java
b/discovery/seata-discovery-raft/src/test/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImplTest.java
index a85619da05..4473e1f481 100644
---
a/discovery/seata-discovery-raft/src/test/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImplTest.java
+++
b/discovery/seata-discovery-raft/src/test/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImplTest.java
@@ -25,10 +25,14 @@ import okhttp3.ResponseBody;
import org.apache.http.HttpStatus;
import org.apache.seata.common.exception.NotSupportYetException;
import org.apache.seata.common.exception.ParseEndpointException;
+import org.apache.seata.common.exception.RetryableException;
+import org.apache.seata.common.metadata.ClusterRole;
+import org.apache.seata.common.metadata.ClusterWatchEvent;
import org.apache.seata.common.metadata.Metadata;
import org.apache.seata.common.metadata.MetadataResponse;
import org.apache.seata.common.metadata.Node;
import org.apache.seata.common.util.HttpClientUtil;
+import org.apache.seata.common.util.SeataHttpWatch;
import org.apache.seata.config.ConfigurationFactory;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
@@ -44,6 +48,7 @@ import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
@@ -63,7 +68,10 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class RaftRegistryServiceImplTest {
@@ -91,12 +99,18 @@ class RaftRegistryServiceImplTest {
}
@AfterEach
- public void tearDown() throws NoSuchFieldException, IllegalAccessException
{
+ public void tearDown() throws Exception {
// Reset the CLOSED flag after each test
Field closedField =
RaftRegistryServiceImpl.class.getDeclaredField("CLOSED");
closedField.setAccessible(true);
AtomicBoolean closed = (AtomicBoolean) closedField.get(null);
closed.set(false);
+
+ Method closeHttp2WatchMethod =
RaftRegistryServiceImpl.class.getDeclaredMethod("closeHttp2Watch");
+ closeHttp2WatchMethod.setAccessible(true);
+ closeHttp2WatchMethod.invoke(null);
+
+ setStaticField("HTTP2_WATCH_GROUP", null);
}
/**
@@ -955,87 +969,22 @@ class RaftRegistryServiceImplTest {
}
/**
- * Test watch method with null response
+ * Test watch method with null response.
+ * Note: this also covers the previous "null status line" scenario because
+ * both cases are represented as null okhttp response in current code path.
*/
@Test
public void watchWithNullResponseTest() throws Exception {
- String jsonString =
-
"{\"nodes\":[{\"control\":{\"host\":\"localhost\",\"port\":7091},\"transaction\":{\"host\":\"localhost\",\"port\":8091},\"group\":\"default\",\"role\":\"LEADER\"}],\"storeMode\":\"raft\",\"term\":1}";
-
- Field metadataField =
RaftRegistryServiceImpl.class.getDeclaredField("METADATA");
- metadataField.setAccessible(true);
- Metadata metadata = (Metadata) metadataField.get(null);
-
- ObjectMapper objectMapper = new ObjectMapper();
- MetadataResponse metadataResponse = objectMapper.readValue(jsonString,
MetadataResponse.class);
- metadata.refreshMetadata("default", metadataResponse);
-
- Field clusterNameField =
RaftRegistryServiceImpl.class.getDeclaredField("CURRENT_TRANSACTION_CLUSTER_NAME");
- clusterNameField.setAccessible(true);
- clusterNameField.set(null, "default");
-
- Field initAddressesField =
RaftRegistryServiceImpl.class.getDeclaredField("INIT_ADDRESSES");
- initAddressesField.setAccessible(true);
- Map<String, List<InetSocketAddress>> initAddresses =
- (Map<String, List<InetSocketAddress>>)
initAddressesField.get(null);
- List<InetSocketAddress> addressList = new ArrayList<>();
- addressList.add(new InetSocketAddress("localhost", 7091));
- initAddresses.put("default", addressList);
+ prepareWatchClusterContext(
+ "default",
+ metadataResponse(1L, createNode("localhost", 7091, 8091,
"default", ClusterRole.LEADER, "2.6.9")));
try (MockedStatic<HttpClientUtil> mockedStatic =
Mockito.mockStatic(HttpClientUtil.class)) {
when(HttpClientUtil.doPost(anyString(), anyMap(), anyMap(),
anyInt()))
.thenReturn(null);
-
- Method watchMethod =
RaftRegistryServiceImpl.class.getDeclaredMethod("watch");
- watchMethod.setAccessible(true);
- boolean result = (boolean) watchMethod.invoke(null);
+ boolean result = invokeWatch();
assertFalse(result, "Watch should return false when response is
null");
- } finally {
- initAddresses.remove("default");
- }
- }
-
- /**
- * Test watch method with null status line
- */
- @Test
- public void watchWithNullStatusLineTest() throws Exception {
- String jsonString =
-
"{\"nodes\":[{\"control\":{\"host\":\"localhost\",\"port\":7091},\"transaction\":{\"host\":\"localhost\",\"port\":8091},\"group\":\"default\",\"role\":\"LEADER\"}],\"storeMode\":\"raft\",\"term\":1}";
-
- Field metadataField =
RaftRegistryServiceImpl.class.getDeclaredField("METADATA");
- metadataField.setAccessible(true);
- Metadata metadata = (Metadata) metadataField.get(null);
-
- ObjectMapper objectMapper = new ObjectMapper();
- MetadataResponse metadataResponse = objectMapper.readValue(jsonString,
MetadataResponse.class);
- metadata.refreshMetadata("default", metadataResponse);
-
- Field clusterNameField =
RaftRegistryServiceImpl.class.getDeclaredField("CURRENT_TRANSACTION_CLUSTER_NAME");
- clusterNameField.setAccessible(true);
- clusterNameField.set(null, "default");
-
- Field initAddressesField =
RaftRegistryServiceImpl.class.getDeclaredField("INIT_ADDRESSES");
- initAddressesField.setAccessible(true);
- Map<String, List<InetSocketAddress>> initAddresses =
- (Map<String, List<InetSocketAddress>>)
initAddressesField.get(null);
- List<InetSocketAddress> addressList = new ArrayList<>();
- addressList.add(new InetSocketAddress("localhost", 7091));
- initAddresses.put("default", addressList);
-
- try (MockedStatic<HttpClientUtil> mockedStatic =
Mockito.mockStatic(HttpClientUtil.class)) {
- // Return null to simulate null response scenario
- when(HttpClientUtil.doPost(anyString(), anyMap(), anyMap(),
anyInt()))
- .thenReturn(null);
-
- Method watchMethod =
RaftRegistryServiceImpl.class.getDeclaredMethod("watch");
- watchMethod.setAccessible(true);
- boolean result = (boolean) watchMethod.invoke(null);
-
- assertFalse(result, "Watch should return false when response is
null");
- } finally {
- initAddresses.remove("default");
}
}
@@ -1214,6 +1163,177 @@ class RaftRegistryServiceImplTest {
}
}
+ @Test
+ public void watchHttp2CreateAndConsumeUpdateTest() throws Exception {
+ String clusterName = "watchHttp2CreateCluster";
+ String group = "default";
+ prepareWatchClusterContext(
+ clusterName,
+ metadataResponse(1L, createNode("127.0.0.1", 7091, 8091,
group, ClusterRole.LEADER, "2.7.0")));
+ setTokenNotExpired();
+
+ SeataHttpWatch<ClusterWatchEvent> watch = mockWatch();
+ when(watch.next())
+ .thenReturn(newWatchUpdateResponse(
+ group,
+ metadataResponse(2L, createNode("127.0.0.1", 7091,
8091, group, ClusterRole.LEADER, "2.7.0"))));
+
+ try (MockedStatic<HttpClientUtil> mockedStatic =
Mockito.mockStatic(HttpClientUtil.class)) {
+ mockedStatic
+ .when(() -> HttpClientUtil.watchPost(
+ anyString(), anyMap(), anyMap(),
eq(ClusterWatchEvent.class), anyInt()))
+ .thenReturn(watch);
+
+ boolean result = invokeWatchHttp2(clusterName);
+ assertTrue(result, "watchHttp2 should return true when UPDATE term
advances");
+
+ Metadata metadata = (Metadata) getStaticField("METADATA");
+ assertEquals(2L,
metadata.getClusterTerm(clusterName).get(group).longValue());
+ mockedStatic.verify(
+ () -> HttpClientUtil.watchPost(
+ anyString(), anyMap(), anyMap(),
eq(ClusterWatchEvent.class), anyInt()),
+ times(1));
+ }
+ }
+
+ @Test
+ public void watchHttp2ReuseForSameGroupTest() throws Exception {
+ String clusterName = "watchHttp2ReuseCluster";
+ String group = "default";
+ prepareWatchClusterContext(
+ clusterName,
+ metadataResponse(1L, createNode("127.0.0.1", 7191, 8191,
group, ClusterRole.LEADER, "2.7.0")));
+ setTokenNotExpired();
+
+ SeataHttpWatch<ClusterWatchEvent> watch = mockWatch();
+ when(watch.next())
+ .thenReturn(newWatchUpdateResponse(
+ group,
+ metadataResponse(2L, createNode("127.0.0.1", 7191,
8191, group, ClusterRole.LEADER, "2.7.0"))))
+ .thenReturn(newWatchUpdateResponse(
+ group,
+ metadataResponse(3L, createNode("127.0.0.1", 7191,
8191, group, ClusterRole.LEADER, "2.7.0"))));
+
+ try (MockedStatic<HttpClientUtil> mockedStatic =
Mockito.mockStatic(HttpClientUtil.class)) {
+ mockedStatic
+ .when(() -> HttpClientUtil.watchPost(
+ anyString(), anyMap(), anyMap(),
eq(ClusterWatchEvent.class), anyInt()))
+ .thenReturn(watch);
+
+ assertTrue(invokeWatchHttp2(clusterName));
+ assertTrue(invokeWatchHttp2(clusterName));
+
+ mockedStatic.verify(
+ () -> HttpClientUtil.watchPost(
+ anyString(), anyMap(), anyMap(),
eq(ClusterWatchEvent.class), anyInt()),
+ times(1));
+ }
+ }
+
+ @Test
+ public void watchHttp2SwitchGroupStableSelectionTest() throws Exception {
+ String clusterName = "watchHttp2MultiGroupCluster";
+ Node group2Leader = createNode("127.0.0.1", 7092, 8092, "g2",
ClusterRole.LEADER, "2.7.0");
+ Node group1Leader = createNode("127.0.0.1", 7091, 8091, "g1",
ClusterRole.LEADER, "2.7.0");
+ prepareWatchClusterContext(clusterName, metadataResponse(1L,
group2Leader), metadataResponse(1L, group1Leader));
+ setTokenNotExpired();
+ setStaticField("HTTP2_WATCH_GROUP", "missing-group");
+
+ SeataHttpWatch<ClusterWatchEvent> watch = mockWatch();
+ when(watch.next())
+ .thenReturn(newWatchUpdateResponse("g1", metadataResponse(2L,
group1Leader)))
+ .thenReturn(newWatchUpdateResponse("g1", metadataResponse(3L,
group1Leader)));
+
+ try (MockedStatic<HttpClientUtil> mockedStatic =
Mockito.mockStatic(HttpClientUtil.class)) {
+ mockedStatic
+ .when(() -> HttpClientUtil.watchPost(
+ anyString(), anyMap(), anyMap(),
eq(ClusterWatchEvent.class), anyInt()))
+ .thenReturn(watch);
+
+ assertTrue(invokeWatchHttp2(clusterName));
+ assertTrue(invokeWatchHttp2(clusterName));
+
+ mockedStatic.verify(
+ () -> HttpClientUtil.watchPost(
+ eq("http://127.0.0.1:7091/metadata/v1/watch"),
+ anyMap(),
+ anyMap(),
+ eq(ClusterWatchEvent.class),
+ anyInt()),
+ times(1));
+ }
+ }
+
+ @Test
+ public void watchHttp2RuntimeExceptionCloseAndRetryableTest() throws
Exception {
+ String clusterName = "watchHttp2ExceptionCluster";
+ String group = "default";
+ prepareWatchClusterContext(
+ clusterName,
+ metadataResponse(1L, createNode("127.0.0.1", 7391, 8391,
group, ClusterRole.LEADER, "2.7.0")));
+ setTokenNotExpired();
+
+ SeataHttpWatch<ClusterWatchEvent> watch = mockWatch();
+ when(watch.next()).thenThrow(new RuntimeException("watch stream
failure"));
+
+ try (MockedStatic<HttpClientUtil> mockedStatic =
Mockito.mockStatic(HttpClientUtil.class)) {
+ mockedStatic
+ .when(() -> HttpClientUtil.watchPost(
+ anyString(), anyMap(), anyMap(),
eq(ClusterWatchEvent.class), anyInt()))
+ .thenReturn(watch);
+
+ assertThrows(RetryableException.class, () ->
invokeWatchHttp2(clusterName));
+ verify(watch, times(1)).close();
+ assertNull(getStaticField("HTTP2_WATCH"));
+ assertNull(getStaticField("HTTP2_WATCH_GROUP"));
+ }
+ }
+
+ @Test
+ public void watchHttp2ClosedStateNoRetryNoiseTest() throws Exception {
+ String clusterName = "watchHttp2ClosedCluster";
+ String group = "default";
+ prepareWatchClusterContext(
+ clusterName,
+ metadataResponse(1L, createNode("127.0.0.1", 7491, 8491,
group, ClusterRole.LEADER, "2.7.0")));
+ setTokenNotExpired();
+ setClosed(true);
+
+ SeataHttpWatch<ClusterWatchEvent> watch = mockWatch();
+ when(watch.next()).thenThrow(new RuntimeException("closed during
shutdown"));
+
+ try (MockedStatic<HttpClientUtil> mockedStatic =
Mockito.mockStatic(HttpClientUtil.class)) {
+ mockedStatic
+ .when(() -> HttpClientUtil.watchPost(
+ anyString(), anyMap(), anyMap(),
eq(ClusterWatchEvent.class), anyInt()))
+ .thenReturn(watch);
+
+ boolean result = invokeWatchHttp2(clusterName);
+ assertFalse(result, "watchHttp2 should return false instead of
retrying when client is closed");
+ verify(watch, times(1)).close();
+ } finally {
+ setClosed(false);
+ }
+ }
+
+ @Test
+ public void selectWatchGroupReuseAndSortedFallbackTest() throws Exception {
+ Method selectWatchGroupMethod =
RaftRegistryServiceImpl.class.getDeclaredMethod("selectWatchGroup", Map.class);
+ selectWatchGroupMethod.setAccessible(true);
+
+ Map<String, Long> groupTerms = new HashMap<>();
+ groupTerms.put("g2", 1L);
+ groupTerms.put("g1", 1L);
+
+ setStaticField("HTTP2_WATCH_GROUP", "g2");
+ assertEquals("g2", selectWatchGroupMethod.invoke(null, groupTerms));
+
+ setStaticField("HTTP2_WATCH_GROUP", "unknown");
+ assertEquals("g1", selectWatchGroupMethod.invoke(null, groupTerms));
+
+ assertNull(selectWatchGroupMethod.invoke(null,
Collections.emptyMap()));
+ }
+
/**
* Test lookup with empty raft cluster address
*/
@@ -1287,6 +1407,304 @@ class RaftRegistryServiceImplTest {
}
}
+ @Test
+ public void supportsHttp2VersionThresholdTest() throws Exception {
+ Method supportsHttp2Method =
RaftRegistryServiceImpl.class.getDeclaredMethod("supportsHttp2", Node.class);
+ supportsHttp2Method.setAccessible(true);
+
+ Node nullVersion = new Node();
+ nullVersion.setVersion(null);
+ assertFalse((boolean) supportsHttp2Method.invoke(null, nullVersion));
+
+ Node blankVersion = new Node();
+ blankVersion.setVersion("");
+ assertFalse((boolean) supportsHttp2Method.invoke(null, blankVersion));
+
+ Node invalidVersion = new Node();
+ invalidVersion.setVersion("invalid-version");
+ assertFalse((boolean) supportsHttp2Method.invoke(null,
invalidVersion));
+
+ Node v269 = new Node();
+ v269.setVersion("2.6.9");
+ assertFalse((boolean) supportsHttp2Method.invoke(null, v269));
+
+ Node v270 = new Node();
+ v270.setVersion("2.7.0");
+ assertTrue((boolean) supportsHttp2Method.invoke(null, v270));
+
+ Node v270Snapshot = new Node();
+ v270Snapshot.setVersion("2.7.0-SNAPSHOT");
+ assertTrue((boolean) supportsHttp2Method.invoke(null, v270Snapshot));
+
+ Node v280 = new Node();
+ v280.setVersion("2.8.0");
+ assertTrue((boolean) supportsHttp2Method.invoke(null, v280));
+ }
+
+ @Test
+ public void shouldRefreshMetadataFilterInvalidEventTest() throws Exception
{
+ Method shouldRefreshMetadata =
RaftRegistryServiceImpl.class.getDeclaredMethod(
+ "shouldRefreshMetadata", String.class, String.class,
SeataHttpWatch.Response.class);
+ shouldRefreshMetadata.setAccessible(true);
+
+ assertFalse((boolean) shouldRefreshMetadata.invoke(null,
"invalidEventCluster", "default", null));
+
+ SeataHttpWatch.Response<ClusterWatchEvent> errorResponse =
+ new
SeataHttpWatch.Response<>(SeataHttpWatch.Response.Type.ERROR, null);
+ assertFalse((boolean) shouldRefreshMetadata.invoke(null,
"invalidEventCluster", "default", errorResponse));
+
+ SeataHttpWatch.Response<ClusterWatchEvent> nullObjectUpdate =
+ new
SeataHttpWatch.Response<>(SeataHttpWatch.Response.Type.UPDATE, null);
+ assertFalse((boolean) shouldRefreshMetadata.invoke(null,
"invalidEventCluster", "default", nullObjectUpdate));
+
+ ClusterWatchEvent nullMetadataEvent = new ClusterWatchEvent();
+ SeataHttpWatch.Response<ClusterWatchEvent> nullMetadataResponse =
+ new
SeataHttpWatch.Response<>(SeataHttpWatch.Response.Type.UPDATE,
nullMetadataEvent);
+ assertFalse(
+ (boolean) shouldRefreshMetadata.invoke(null,
"invalidEventCluster", "default", nullMetadataResponse));
+
+ MetadataResponse emptyNodesMetadata = new MetadataResponse();
+ emptyNodesMetadata.setNodes(Collections.emptyList());
+ emptyNodesMetadata.setStoreMode("raft");
+ emptyNodesMetadata.setTerm(1L);
+ ClusterWatchEvent emptyNodesEvent = new ClusterWatchEvent();
+ emptyNodesEvent.setGroup("default");
+ emptyNodesEvent.setMetadata(emptyNodesMetadata);
+ SeataHttpWatch.Response<ClusterWatchEvent> emptyNodesResponse =
+ new
SeataHttpWatch.Response<>(SeataHttpWatch.Response.Type.UPDATE, emptyNodesEvent);
+ assertFalse((boolean) shouldRefreshMetadata.invoke(null,
"invalidEventCluster", "default", emptyNodesResponse));
+ }
+
+ @Test
+ public void shouldRefreshMetadataOnTermOrNodeChangeTest() throws Exception
{
+ Field metadataField =
RaftRegistryServiceImpl.class.getDeclaredField("METADATA");
+ metadataField.setAccessible(true);
+ Metadata metadata = (Metadata) metadataField.get(null);
+
+ Method shouldRefreshMetadata =
RaftRegistryServiceImpl.class.getDeclaredMethod(
+ "shouldRefreshMetadata", String.class, String.class,
SeataHttpWatch.Response.class);
+ shouldRefreshMetadata.setAccessible(true);
+
+ String termAdvancedCluster = "termAdvancedCluster";
+ metadata.refreshMetadata(
+ termAdvancedCluster,
+ metadataResponse(1L, createNode("127.0.0.1", 7091, 8091,
"default", ClusterRole.LEADER, "2.7.0")));
+ ClusterWatchEvent termAdvancedEvent = new ClusterWatchEvent();
+ termAdvancedEvent.setGroup("default");
+ termAdvancedEvent.setMetadata(
+ metadataResponse(2L, createNode("127.0.0.1", 7091, 8091,
"default", ClusterRole.LEADER, "2.7.0")));
+ SeataHttpWatch.Response<ClusterWatchEvent> termAdvancedResponse =
+ new
SeataHttpWatch.Response<>(SeataHttpWatch.Response.Type.UPDATE,
termAdvancedEvent);
+ assertTrue((boolean) shouldRefreshMetadata.invoke(null,
termAdvancedCluster, "default", termAdvancedResponse));
+
+ String unchangedCluster = "unchangedCluster";
+ metadata.refreshMetadata(
+ unchangedCluster,
+ metadataResponse(3L, createNode("127.0.0.1", 7092, 8092,
"default", ClusterRole.LEADER, "2.7.0")));
+ ClusterWatchEvent unchangedEvent = new ClusterWatchEvent();
+ unchangedEvent.setGroup("default");
+ unchangedEvent.setMetadata(
+ metadataResponse(3L, createNode("127.0.0.1", 7092, 8092,
"default", ClusterRole.LEADER, "2.7.0")));
+ SeataHttpWatch.Response<ClusterWatchEvent> unchangedResponse =
+ new
SeataHttpWatch.Response<>(SeataHttpWatch.Response.Type.UPDATE, unchangedEvent);
+ assertFalse((boolean) shouldRefreshMetadata.invoke(null,
unchangedCluster, "default", unchangedResponse));
+
+ String nodeChangedCluster = "nodeChangedCluster";
+ metadata.refreshMetadata(
+ nodeChangedCluster,
+ metadataResponse(5L, createNode("127.0.0.1", 7093, 8093,
"default", ClusterRole.LEADER, "2.7.0")));
+ ClusterWatchEvent nodeChangedEvent = new ClusterWatchEvent();
+ nodeChangedEvent.setGroup("default");
+ nodeChangedEvent.setMetadata(
+ metadataResponse(5L, createNode("127.0.0.1", 7193, 8193,
"default", ClusterRole.LEADER, "2.7.0")));
+ SeataHttpWatch.Response<ClusterWatchEvent> nodeChangedResponse =
+ new
SeataHttpWatch.Response<>(SeataHttpWatch.Response.Type.UPDATE,
nodeChangedEvent);
+ assertTrue((boolean) shouldRefreshMetadata.invoke(null,
nodeChangedCluster, "default", nodeChangedResponse));
+
+ String staleTermCluster = "staleTermCluster";
+ Node currentLeader = createNode("127.0.0.1", 7094, 8094, "default",
ClusterRole.LEADER, "2.7.0");
+ metadata.refreshMetadata(staleTermCluster, metadataResponse(10L,
currentLeader));
+ ClusterWatchEvent staleTermEvent = new ClusterWatchEvent();
+ staleTermEvent.setGroup("default");
+ staleTermEvent.setMetadata(
+ metadataResponse(9L, createNode("127.0.0.1", 7194, 8194,
"default", ClusterRole.FOLLOWER, "2.7.0")));
+ SeataHttpWatch.Response<ClusterWatchEvent> staleTermResponse =
+ new
SeataHttpWatch.Response<>(SeataHttpWatch.Response.Type.UPDATE, staleTermEvent);
+
+ assertFalse((boolean) shouldRefreshMetadata.invoke(null,
staleTermCluster, "default", staleTermResponse));
+ assertEquals(
+ 10L,
metadata.getClusterTerm(staleTermCluster).get("default").longValue());
+ assertEquals(
+ currentLeader.getTransaction().getPort(),
+ metadata.getNodes(staleTermCluster, "default")
+ .get(0)
+ .getTransaction()
+ .getPort());
+ }
+
+ @Test
+ public void hasMetadataChangedOrderInsensitiveTest() throws Exception {
+ Field metadataField =
RaftRegistryServiceImpl.class.getDeclaredField("METADATA");
+ metadataField.setAccessible(true);
+ Metadata metadata = (Metadata) metadataField.get(null);
+
+ Method hasMetadataChanged =
RaftRegistryServiceImpl.class.getDeclaredMethod(
+ "hasMetadataChanged", String.class, String.class,
MetadataResponse.class);
+ hasMetadataChanged.setAccessible(true);
+
+ String clusterName = "orderInsensitiveCluster";
+ Node first = createNode("127.0.0.1", 7291, 8291, "default",
ClusterRole.LEADER, "2.7.0");
+ Node second = createNode("127.0.0.1", 7292, 8292, "default",
ClusterRole.FOLLOWER, "2.7.0");
+ metadata.refreshMetadata(clusterName, metadataResponse(10L, first,
second));
+
+ MetadataResponse reversedOrder = metadataResponse(10L, second, first);
+ assertFalse((boolean) hasMetadataChanged.invoke(null, clusterName,
"default", reversedOrder));
+
+ Node roleChanged = createNode("127.0.0.1", 7292, 8292, "default",
ClusterRole.LEADER, "2.7.0");
+ MetadataResponse changedNodeSignature = metadataResponse(10L, first,
roleChanged);
+ assertTrue((boolean) hasMetadataChanged.invoke(null, clusterName,
"default", changedNodeSignature));
+ }
+
+ @Test
+ public void hasMetadataChangedEdgeCasesTest() throws Exception {
+ Metadata metadata = (Metadata) getStaticField("METADATA");
+
+ Method hasMetadataChanged =
RaftRegistryServiceImpl.class.getDeclaredMethod(
+ "hasMetadataChanged", String.class, String.class,
MetadataResponse.class);
+ hasMetadataChanged.setAccessible(true);
+
+ assertFalse((boolean) hasMetadataChanged.invoke(null,
"nullIncomingCluster", "default", null));
+
+ Node first = createNode("127.0.0.1", 7391, 8391, "default",
ClusterRole.LEADER, "2.7.0");
+ assertTrue(
+ (boolean) hasMetadataChanged.invoke(null, "emptyLocalCluster",
"default", metadataResponse(1L, first)));
+
+ assertFalse((boolean) hasMetadataChanged.invoke(null,
"emptyBothCluster", "default", metadataResponse(1L)));
+
+ String termChangedCluster = "termChangedCluster";
+ metadata.refreshMetadata(termChangedCluster, metadataResponse(1L,
first));
+ assertTrue(
+ (boolean) hasMetadataChanged.invoke(null, termChangedCluster,
"default", metadataResponse(2L, first)));
+
+ String nodeSizeChangedCluster = "nodeSizeChangedCluster";
+ metadata.refreshMetadata(nodeSizeChangedCluster, metadataResponse(3L,
first));
+ Node second = createNode("127.0.0.1", 7392, 8392, "default",
ClusterRole.FOLLOWER, "2.7.0");
+ assertTrue((boolean) hasMetadataChanged.invoke(
+ null, nodeSizeChangedCluster, "default", metadataResponse(3L,
first, second)));
+ }
+
+ @Test
+ public void buildNodeSignatureEdgeCasesTest() throws Exception {
+ Method buildNodeSignature =
RaftRegistryServiceImpl.class.getDeclaredMethod("buildNodeSignature",
Node.class);
+ buildNodeSignature.setAccessible(true);
+
+ assertEquals("", buildNodeSignature.invoke(null, new Object[] {null}));
+
+ Node node = new Node();
+ node.setGroup("default");
+ node.setRole(ClusterRole.LEADER);
+ node.setVersion("2.7.0");
+ assertEquals("||LEADER|2.7.0|default", buildNodeSignature.invoke(null,
node));
+ }
+
+ private static void prepareWatchClusterContext(String clusterName,
MetadataResponse... metadataResponses)
+ throws Exception {
+ Metadata metadata = (Metadata) getStaticField("METADATA");
+ for (MetadataResponse metadataResponse : metadataResponses) {
+ metadata.refreshMetadata(clusterName, metadataResponse);
+ }
+ setStaticField("CURRENT_TRANSACTION_CLUSTER_NAME", clusterName);
+ setStaticField("CURRENT_TRANSACTION_SERVICE_GROUP", "tx");
+
+ Map<String, List<InetSocketAddress>> aliveNodes =
+ (Map<String, List<InetSocketAddress>>)
getStaticField("ALIVE_NODES");
+ aliveNodes.remove("tx");
+ }
+
+ private static boolean invokeWatch() throws Exception {
+ Method watchMethod =
RaftRegistryServiceImpl.class.getDeclaredMethod("watch");
+ watchMethod.setAccessible(true);
+ try {
+ return (boolean) watchMethod.invoke(null);
+ } catch (InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof Exception) {
+ throw (Exception) cause;
+ }
+ throw new RuntimeException(cause);
+ }
+ }
+
+ private static boolean invokeWatchHttp2(String clusterName) throws
Exception {
+ Method watchHttp2Method =
RaftRegistryServiceImpl.class.getDeclaredMethod("watchHttp2", String.class);
+ watchHttp2Method.setAccessible(true);
+ try {
+ return (boolean) watchHttp2Method.invoke(null, clusterName);
+ } catch (InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof Exception) {
+ throw (Exception) cause;
+ }
+ throw new RuntimeException(cause);
+ }
+ }
+
+ private static SeataHttpWatch.Response<ClusterWatchEvent>
newWatchUpdateResponse(
+ String group, MetadataResponse metadataResponse) {
+ ClusterWatchEvent event = new ClusterWatchEvent();
+ event.setGroup(group);
+ event.setMetadata(metadataResponse);
+ return new
SeataHttpWatch.Response<>(SeataHttpWatch.Response.Type.UPDATE, event);
+ }
+
+ private static SeataHttpWatch<ClusterWatchEvent> mockWatch() {
+ return mock(SeataHttpWatch.class);
+ }
+
+ private static void setTokenNotExpired() throws Exception {
+ Field tokenTimeStamp =
RaftRegistryServiceImpl.class.getDeclaredField("tokenTimeStamp");
+ tokenTimeStamp.setAccessible(true);
+ tokenTimeStamp.setLong(RaftRegistryServiceImpl.class,
System.currentTimeMillis());
+ }
+
+ private static void setClosed(boolean value) throws Exception {
+ Field closedField =
RaftRegistryServiceImpl.class.getDeclaredField("CLOSED");
+ closedField.setAccessible(true);
+ AtomicBoolean closed = (AtomicBoolean) closedField.get(null);
+ closed.set(value);
+ }
+
+ private static void setStaticField(String fieldName, Object value) throws
Exception {
+ Field field =
RaftRegistryServiceImpl.class.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(null, value);
+ }
+
+ private static Object getStaticField(String fieldName) throws Exception {
+ Field field =
RaftRegistryServiceImpl.class.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return field.get(null);
+ }
+
+ private static Node createNode(
+ String host, int controlPort, int transactionPort, String group,
ClusterRole role, String version) {
+ Node node = new Node();
+ node.setControl(new Node.Endpoint(host, controlPort));
+ node.setTransaction(new Node.Endpoint(host, transactionPort));
+ node.setGroup(group);
+ node.setRole(role);
+ node.setVersion(version);
+ return node;
+ }
+
+ private static MetadataResponse metadataResponse(long term, Node... nodes)
{
+ MetadataResponse metadataResponse = new MetadataResponse();
+ metadataResponse.setNodes(Arrays.asList(nodes));
+ metadataResponse.setStoreMode("raft");
+ metadataResponse.setTerm(term);
+ return metadataResponse;
+ }
+
/**
* Test startQueryMetadata creates thread pool
*/
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]