diff --git a/.gitignore b/.gitignore
index 308540907c1..63a74b7da4f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,6 +46,10 @@ dependency-reduced-pom.xml
/distribution/sessionStore/
/distribution/*/sessionStore/
/file_store/
+/configStore/
+/config/configStore/
+/distribution/configStore/
+/distribution/*/configStore/
# system ignore
.DS_Store
diff --git a/all/pom.xml b/all/pom.xml
index cef3c32c428..47968eafda6 100644
--- a/all/pom.xml
+++ b/all/pom.xml
@@ -91,6 +91,11 @@
seata-config-spring-cloud
${project.version}
+
+ org.apache.seata
+ seata-config-raft
+ ${project.version}
+
org.apache.seata
seata-core
diff --git a/build/pom.xml b/build/pom.xml
index 0b09f55331c..06badf6350f 100644
--- a/build/pom.xml
+++ b/build/pom.xml
@@ -312,6 +312,7 @@
*-pom.xml
**/db_store/**
**/sessionStore/**
+ **/configStore/**
**/root.data
false
diff --git a/common/src/main/java/org/apache/seata/common/ConfigurationKeys.java b/common/src/main/java/org/apache/seata/common/ConfigurationKeys.java
index 0bcb83cf3c3..e322d8cb2f9 100644
--- a/common/src/main/java/org/apache/seata/common/ConfigurationKeys.java
+++ b/common/src/main/java/org/apache/seata/common/ConfigurationKeys.java
@@ -1177,6 +1177,31 @@ public interface ConfigurationKeys {
*/
String ROCKET_MQ_MSG_TIMEOUT = SERVER_PREFIX + "rocketmqMsgTimeout";
+ String CONFIG_STORE_PREFIX = FILE_ROOT_PREFIX_CONFIG + "raft" + FILE_CONFIG_SPLIT_CHAR + "db" + FILE_CONFIG_SPLIT_CHAR;
+
+ /**
+ * The constant CONFIG_STORE_TYPE
+ */
+ String CONFIG_STORE_TYPE = CONFIG_STORE_PREFIX + "type";
+
+ /**
+ * The constant CONFIG_STORE_DIR
+ */
+ String CONFIG_STORE_DIR = CONFIG_STORE_PREFIX + "dir";
+
+ /**
+ * The constant CONFIG_STORE_DESTROY_ON_SHUTDOWN
+ */
+ String CONFIG_STORE_DESTROY_ON_SHUTDOWN = CONFIG_STORE_PREFIX + "destroyOnShutdown";
+
+ /**
+ * The constant CONFIG_STORE_NAMESPACE
+ */
+ String CONFIG_STORE_NAMESPACE = CONFIG_STORE_PREFIX + "namespace";
+ /**
+ * The constant CONFIG_STORE_DATA_ID
+ */
+ String CONFIG_STORE_DATA_ID = CONFIG_STORE_PREFIX + "dataId";
/**
*
*/
@@ -1247,4 +1272,5 @@ public interface ConfigurationKeys {
*/
String SERVER_REGISTRY_METADATA_EXTERNAL = SERVER_REGISTRY_METADATA_PREFIX + ".external";
+
}
diff --git a/common/src/main/java/org/apache/seata/common/Constants.java b/common/src/main/java/org/apache/seata/common/Constants.java
index 812a3605e46..c037d95a2ff 100644
--- a/common/src/main/java/org/apache/seata/common/Constants.java
+++ b/common/src/main/java/org/apache/seata/common/Constants.java
@@ -229,6 +229,36 @@ public interface Constants {
*/
String JACKSON_JSON_TEXT_PREFIX = "{\"@class\":";
+ /**
+ * The constant APPLICATION_TYPE_KEY
+ */
+ String APPLICATION_TYPE_KEY = "application.type";
+
+ /**
+ * The constant APPLICATION_TYPE_SERVER
+ */
+ String APPLICATION_TYPE_SERVER = "server";
+
+ /**
+ * The constant APPLICATION_TYPE_CLIENT
+ */
+ String APPLICATION_TYPE_CLIENT = "client";
+
+ /**
+ * The constant DEFAULT_STORE_NAMESPACE in raft configuration
+ */
+
+ String DEFAULT_STORE_NAMESPACE = "default";
+ /**
+ * The constant DEFAULT_STORE_DATA_ID in raft configuration
+ */
+ String DEFAULT_STORE_DATA_ID = "seata.properties";
+
+ /**
+ * The constant RAFT_CONFIG_GROUP
+ */
+ String RAFT_CONFIG_GROUP = "config";
+
/**
* The constant DEAD_LOCK_SQL_STATE
*/
@@ -239,4 +269,8 @@ public interface Constants {
*/
int DEAD_LOCK_ERROR_CODE = 1213;
+ /**
+ * The constant CONFIGURATION_META_FILE_NAME
+ */
+ String CONFIGURATION_META_FILE_NAME = "configuration-meta.yml";
}
diff --git a/common/src/main/java/org/apache/seata/common/DefaultValues.java b/common/src/main/java/org/apache/seata/common/DefaultValues.java
index 0c3480bab16..55ed8f3fa9f 100644
--- a/common/src/main/java/org/apache/seata/common/DefaultValues.java
+++ b/common/src/main/java/org/apache/seata/common/DefaultValues.java
@@ -507,20 +507,79 @@ public interface DefaultValues {
*/
int DEFAULT_ROCKET_MQ_MSG_TIMEOUT = 60 * 1000;
+ /**
+ * The constant DEFAULT_DB_STORE_FILE_DIR.
+ */
+ String DEFAULT_DB_STORE_FILE_DIR = "configStore";
+
+ /**
+ * The constant DEFAULT_DB_TYPE.
+ */
+ String DEFAULT_DB_TYPE = "rocksdb";
+
+ /**
+ * The constant DEFAULT_DB_DRUID_TIME_BETWEEN_EVICTION_RUNS_MILLIS.
+ */
long DEFAULT_DB_DRUID_TIME_BETWEEN_EVICTION_RUNS_MILLIS = 120000;
+
+ /**
+ * The constant DEFAULT_DB_DRUID_MIN_EVICTABLE_TIME_MILLIS.
+ */
long DEFAULT_DB_DRUID_MIN_EVICTABLE_TIME_MILLIS = 300000;
+
+ /**
+ * The constant DEFAULT_DB_DRUID_TEST_WHILE_IDLE.
+ */
boolean DEFAULT_DB_DRUID_TEST_WHILE_IDLE = true;
+
+ /**
+ * The constant DEFAULT_DB_DRUID_TEST_ON_BORROW.
+ */
boolean DEFAULT_DB_DRUID_TEST_ON_BORROW = false;
+
+ /**
+ * The constant DEFAULT_DB_DRUID_KEEP_ALIVE.
+ */
boolean DEFAULT_DB_DRUID_KEEP_ALIVE = false;
-
+
+ /**
+ * The constant DEFAULT_DB_HIKARI_IDLE_TIMEOUT.
+ */
long DEFAULT_DB_HIKARI_IDLE_TIMEOUT = 600000L;
+
+ /**
+ * The constant DEFAULT_DB_HIKARI_KEEPALIVE_TIME.
+ */
long DEFAULT_DB_HIKARI_KEEPALIVE_TIME = 120000L;
+
+ /**
+ * The constant DEFAULT_DB_HIKARI_MAX_LIFE_TIME.
+ */
long DEFAULT_DB_HIKARI_MAX_LIFE_TIME = 1800000L;
+
+ /**
+ * The constant DEFAULT_DB_HIKARI_VALIDATION_TIMEOUT.
+ */
long DEFAULT_DB_HIKARI_VALIDATION_TIMEOUT = 5000L;
+ /**
+ * The constant DEFAULT_DB_DBCP_TIME_BETWEEN_EVICTION_RUNS_MILLIS.
+ */
long DEFAULT_DB_DBCP_TIME_BETWEEN_EVICTION_RUNS_MILLIS = 120000;
+
+ /**
+ * The constant DEFAULT_DB_DBCP_MIN_EVICTABLE_TIME_MILLIS.
+ */
long DEFAULT_DB_DBCP_MIN_EVICTABLE_TIME_MILLIS = 300000;
+
+ /**
+ * The constant DEFAULT_DB_DBCP_TEST_WHILE_IDLE.
+ */
boolean DEFAULT_DB_DBCP_TEST_WHILE_IDLE = true;
+
+ /**
+ * The constant DEFAULT_DB_DBCP_TEST_ON_BORROW.
+ */
boolean DEFAULT_DB_DBCP_TEST_ON_BORROW = false;
/**
diff --git a/common/src/main/java/org/apache/seata/common/config/ConfigDataResponse.java b/common/src/main/java/org/apache/seata/common/config/ConfigDataResponse.java
new file mode 100644
index 00000000000..577e1431772
--- /dev/null
+++ b/common/src/main/java/org/apache/seata/common/config/ConfigDataResponse.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.common.config;
+
+import java.io.Serializable;
+
+public class ConfigDataResponse implements Serializable {
+ private static final long serialVersionUID = -1959848221874923781L;
+ private T result;
+ private String errMsg;
+ private Boolean success;
+
+ public T getResult() {
+ return result;
+ }
+
+ public void setResult(T result) {
+ this.result = result;
+ }
+
+ public String getErrMsg() {
+ return errMsg;
+ }
+
+ public void setErrMsg(String errMsg) {
+ this.errMsg = errMsg;
+ }
+
+ public Boolean getSuccess() {
+ return success;
+ }
+
+ public void setSuccess(Boolean success) {
+ this.success = success;
+ }
+
+ @Override
+ public String toString() {
+ return "ConfigDataResponse{" +
+ "result=" + result +
+ ", errMsg='" + errMsg + '\'' +
+ ", success=" + success +
+ '}';
+ }
+}
diff --git a/common/src/main/java/org/apache/seata/common/metadata/MetadataResponse.java b/common/src/main/java/org/apache/seata/common/metadata/MetadataResponse.java
index 609402947fb..0432cc21613 100644
--- a/common/src/main/java/org/apache/seata/common/metadata/MetadataResponse.java
+++ b/common/src/main/java/org/apache/seata/common/metadata/MetadataResponse.java
@@ -25,6 +25,8 @@ public class MetadataResponse {
String storeMode;
+ String configMode;
+
long term;
public List getNodes() {
@@ -50,5 +52,12 @@ public long getTerm() {
public void setTerm(long term) {
this.term = term;
}
-
+
+ public String getConfigMode() {
+ return configMode;
+ }
+
+ public void setConfigMode(String configMode) {
+ this.configMode = configMode;
+ }
}
diff --git a/common/src/main/java/org/apache/seata/common/store/StoreMode.java b/common/src/main/java/org/apache/seata/common/store/StoreMode.java
index 29c90d544f2..68a5092fd16 100644
--- a/common/src/main/java/org/apache/seata/common/store/StoreMode.java
+++ b/common/src/main/java/org/apache/seata/common/store/StoreMode.java
@@ -16,6 +16,8 @@
*/
package org.apache.seata.common.store;
+import org.apache.seata.common.util.StringUtils;
+
/**
* transaction log store mode
*
@@ -55,6 +57,9 @@ public enum StoreMode {
* @return the store mode
*/
public static StoreMode get(String name) {
+ if (StringUtils.isEmpty(name)) {
+ return null;
+ }
for (StoreMode sm : StoreMode.class.getEnumConstants()) {
if (sm.name.equalsIgnoreCase(name)) {
return sm;
diff --git a/common/src/main/java/org/apache/seata/common/util/NumberUtils.java b/common/src/main/java/org/apache/seata/common/util/NumberUtils.java
index 7374bdc08c5..fdd91addff8 100644
--- a/common/src/main/java/org/apache/seata/common/util/NumberUtils.java
+++ b/common/src/main/java/org/apache/seata/common/util/NumberUtils.java
@@ -60,4 +60,22 @@ public static Long toLong(String str) {
}
return null;
}
+
+ public static byte[] longToBytes(long x) {
+ byte[] result = new byte[8];
+ for (int i = 7; i >= 0; i--) {
+ result[i] = (byte)(x & 0xFF);
+ x >>= 8;
+ }
+ return result;
+ }
+
+ public static long bytesToLong(byte[] bytes) {
+ long result = 0;
+ for (int i = 0; i < 8; i++) {
+ result <<= 8;
+ result |= bytes[i] & 0xFF;
+ }
+ return result;
+ }
}
diff --git a/common/src/test/java/org/apache/seata/common/util/NumberUtilsTest.java b/common/src/test/java/org/apache/seata/common/util/NumberUtilsTest.java
index 828f7c2fe26..1db14ee78b5 100644
--- a/common/src/test/java/org/apache/seata/common/util/NumberUtilsTest.java
+++ b/common/src/test/java/org/apache/seata/common/util/NumberUtilsTest.java
@@ -36,4 +36,12 @@ public void testToInReturnDefaultValueWithFormatIsInvalid() {
public void testToInReturnParsedValue() {
Assertions.assertEquals(10, NumberUtils.toInt("10", 9));
}
+
+ @Test
+ public void testBytesAndLong() {
+ Long a = 123456L;
+ byte[] bytes = NumberUtils.longToBytes(a);
+ long b = NumberUtils.bytesToLong(bytes);
+ Assertions.assertEquals(a, b);
+ }
}
diff --git a/config/pom.xml b/config/pom.xml
index 3ed784fb152..a0d76aa6e9c 100644
--- a/config/pom.xml
+++ b/config/pom.xml
@@ -41,5 +41,6 @@
seata-config-etcd3
seata-config-consul
seata-config-spring-cloud
+ seata-config-raft
diff --git a/config/seata-config-all/pom.xml b/config/seata-config-all/pom.xml
index 0eceb6a2e82..1818bffcd3c 100644
--- a/config/seata-config-all/pom.xml
+++ b/config/seata-config-all/pom.xml
@@ -60,6 +60,11 @@
seata-config-spring-cloud
${project.version}
+
+ ${project.groupId}
+ seata-config-raft
+ ${project.version}
+
diff --git a/config/seata-config-core/pom.xml b/config/seata-config-core/pom.xml
index 896a7070fe3..501276ff505 100644
--- a/config/seata-config-core/pom.xml
+++ b/config/seata-config-core/pom.xml
@@ -43,6 +43,11 @@
org.yaml
snakeyaml
+
+ org.rocksdb
+ rocksdbjni
+
+
diff --git a/config/seata-config-core/src/main/java/org/apache/seata/config/ConfigType.java b/config/seata-config-core/src/main/java/org/apache/seata/config/ConfigType.java
index 869d2d19c40..441e30c65b1 100644
--- a/config/seata-config-core/src/main/java/org/apache/seata/config/ConfigType.java
+++ b/config/seata-config-core/src/main/java/org/apache/seata/config/ConfigType.java
@@ -49,6 +49,10 @@ public enum ConfigType {
* spring cloud config type
*/
SpringCloudConfig,
+ /**
+ * Raft config type
+ */
+ Raft,
/**
* Custom config type
*/
diff --git a/config/seata-config-core/src/main/java/org/apache/seata/config/ConfigurationChangeEvent.java b/config/seata-config-core/src/main/java/org/apache/seata/config/ConfigurationChangeEvent.java
index 714f8b17678..c3f7076aa8b 100644
--- a/config/seata-config-core/src/main/java/org/apache/seata/config/ConfigurationChangeEvent.java
+++ b/config/seata-config-core/src/main/java/org/apache/seata/config/ConfigurationChangeEvent.java
@@ -38,6 +38,10 @@ public ConfigurationChangeEvent(String dataId, String newValue) {
this(dataId, DEFAULT_NAMESPACE, null, newValue, ConfigurationChangeType.MODIFY);
}
+ public ConfigurationChangeEvent(String namespace, String dataId, String newValue) {
+ this(dataId, namespace, null, newValue, ConfigurationChangeType.MODIFY);
+ }
+
public ConfigurationChangeEvent(String dataId, String namespace, String oldValue, String newValue,
ConfigurationChangeType type) {
this.dataId = dataId;
diff --git a/config/seata-config-core/src/main/java/org/apache/seata/config/dto/ConfigurationInfoDto.java b/config/seata-config-core/src/main/java/org/apache/seata/config/dto/ConfigurationInfoDto.java
new file mode 100644
index 00000000000..6dff2018856
--- /dev/null
+++ b/config/seata-config-core/src/main/java/org/apache/seata/config/dto/ConfigurationInfoDto.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.config.dto;
+
+import java.io.Serializable;
+import java.util.Map;
+
+public class ConfigurationInfoDto implements Serializable {
+ private static final long serialVersionUID = 72337179613855724L;
+
+ private Map config;
+
+ private Long version;
+
+ public Map getConfig() {
+ return config;
+ }
+
+ public void setConfig(Map config) {
+ this.config = config;
+ }
+
+ public Long getVersion() {
+ return version;
+ }
+
+ public void setVersion(Long version) {
+ this.version = version;
+ }
+}
diff --git a/config/seata-config-core/src/main/java/org/apache/seata/config/dto/ConfigurationItem.java b/config/seata-config-core/src/main/java/org/apache/seata/config/dto/ConfigurationItem.java
new file mode 100644
index 00000000000..6b47cb801e9
--- /dev/null
+++ b/config/seata-config-core/src/main/java/org/apache/seata/config/dto/ConfigurationItem.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.config.dto;
+
+import java.io.Serializable;
+
+/**
+ * The configuration items
+ *
+ */
+public class ConfigurationItem implements Serializable {
+ private static final long serialVersionUID = 32787493713855767L;
+ private String key;
+ private Object value;
+ private String description;
+ private Object defaultValue;
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+
+ public void setValue(Object value) {
+ this.value = value;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public Object getDefaultValue() {
+ return defaultValue;
+ }
+
+ public void setDefaultValue(Object defaultValue) {
+ this.defaultValue = defaultValue;
+ }
+
+
+}
diff --git a/config/seata-config-core/src/main/java/org/apache/seata/config/dto/ConfigurationItemMeta.java b/config/seata-config-core/src/main/java/org/apache/seata/config/dto/ConfigurationItemMeta.java
new file mode 100644
index 00000000000..d7c493d7879
--- /dev/null
+++ b/config/seata-config-core/src/main/java/org/apache/seata/config/dto/ConfigurationItemMeta.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.config.dto;
+
+import java.io.Serializable;
+
+/**
+ * The configuration items meta
+ *
+ */
+public class ConfigurationItemMeta implements Serializable {
+
+ private static final long serialVersionUID = 8771878731395411166L;
+ private final String key;
+ private final String description;
+ private final Object defaultValue;
+ private final Boolean isEncrypt;
+
+ public ConfigurationItemMeta(String key, String description, Object defaultValue, Boolean isEncrypt) {
+ this.key = key;
+ this.description = description;
+ this.defaultValue = defaultValue;
+ this.isEncrypt = isEncrypt;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public Object getDefaultValue() {
+ return defaultValue;
+ }
+ public Boolean getEncrypt() {
+ return isEncrypt;
+ }
+}
diff --git a/config/seata-config-core/src/main/java/org/apache/seata/config/store/ConfigStoreManager.java b/config/seata-config-core/src/main/java/org/apache/seata/config/store/ConfigStoreManager.java
new file mode 100644
index 00000000000..9fab926a52b
--- /dev/null
+++ b/config/seata-config-core/src/main/java/org/apache/seata/config/store/ConfigStoreManager.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.config.store;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.seata.common.util.CollectionUtils;
+import org.apache.seata.common.util.StringUtils;
+import org.apache.seata.config.ConfigurationChangeListener;
+import org.apache.seata.config.processor.ConfigDataType;
+import org.apache.seata.config.processor.ConfigProcessor;
+
+/**
+ * The interface Local config store manager.
+ *
+ */
+public interface ConfigStoreManager {
+ String get(String namespace, String dataId, String key);
+
+ Map getAll(String namespace, String dataId);
+
+ Boolean put(String namespace, String dataId, String key, Object value);
+
+ Boolean delete(String namespace, String dataId, String key);
+
+ Boolean putAll(String namespace, String dataId, Map configMap);
+
+ Boolean deleteAll(String namespace, String dataId);
+
+ Boolean isEmpty(String namespace, String dataId);
+
+ Map> getConfigMap();
+
+ Boolean putConfigMap(Map> configMap);
+
+ Boolean clearData();
+
+ List getAllNamespaces();
+
+ List getAllDataIds(String namespace);
+
+ Long getConfigVersion(String namespace, String dataId);
+
+ Boolean putConfigVersion(String namespace, String dataId, Long version);
+
+ Boolean deleteConfigVersion(String namespace, String dataId);
+ void destroy();
+ void shutdown();
+
+ default void addConfigListener(String group, String dataId, ConfigurationChangeListener listener) {};
+
+ default void removeConfigListener(String group, String dataId, ConfigurationChangeListener listener) {};
+
+ static String convertConfig2Str(Map configs) {
+ StringBuilder sb = new StringBuilder();
+ if (CollectionUtils.isEmpty(configs)) {
+ sb.toString();
+ }
+ for (Map.Entry entry : configs.entrySet()) {
+ sb.append(entry.getKey()).append("=").append(entry.getValue().toString()).append("\n");
+ }
+ return sb.toString();
+ }
+
+ static Map convertConfigStr2Map(String configStr) {
+ if (StringUtils.isEmpty(configStr)) {
+ return new HashMap<>();
+ }
+ Map configs = new HashMap<>();
+ try {
+ Properties properties = ConfigProcessor.processConfig(configStr, ConfigDataType.properties.name());
+ properties.forEach((k, v) -> configs.put(k.toString(), v));
+ return configs;
+ } catch (IOException e) {
+ return new HashMap<>();
+ }
+ }
+}
diff --git a/config/seata-config-core/src/main/java/org/apache/seata/config/store/ConfigStoreManagerFactory.java b/config/seata-config-core/src/main/java/org/apache/seata/config/store/ConfigStoreManagerFactory.java
new file mode 100644
index 00000000000..1f692893909
--- /dev/null
+++ b/config/seata-config-core/src/main/java/org/apache/seata/config/store/ConfigStoreManagerFactory.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.config.store;
+
+import java.util.Objects;
+
+import org.apache.seata.common.loader.EnhancedServiceLoader;
+import org.apache.seata.config.Configuration;
+import org.apache.seata.config.ConfigurationFactory;
+
+import static org.apache.seata.common.ConfigurationKeys.CONFIG_STORE_TYPE;
+import static org.apache.seata.common.DefaultValues.DEFAULT_DB_TYPE;
+
+public class ConfigStoreManagerFactory {
+ private static final Configuration FILE_CONFIG = ConfigurationFactory.CURRENT_FILE_INSTANCE;
+ private static volatile ConfigStoreManager instance;
+
+ public static ConfigStoreManager getInstance() {
+ if (instance == null) {
+ synchronized (ConfigStoreManagerFactory.class) {
+ if (instance == null) {
+ String dbType = FILE_CONFIG.getConfig(CONFIG_STORE_TYPE, DEFAULT_DB_TYPE);
+ instance = EnhancedServiceLoader.load(ConfigStoreManagerProvider.class, Objects.requireNonNull(dbType), false).provide();
+ }
+ }
+ }
+ return instance;
+ }
+
+ public static void destroy() {
+ if (instance != null) {
+ instance.shutdown();
+ }
+ }
+}
diff --git a/config/seata-config-core/src/main/java/org/apache/seata/config/store/ConfigStoreManagerProvider.java b/config/seata-config-core/src/main/java/org/apache/seata/config/store/ConfigStoreManagerProvider.java
new file mode 100644
index 00000000000..15aededefd5
--- /dev/null
+++ b/config/seata-config-core/src/main/java/org/apache/seata/config/store/ConfigStoreManagerProvider.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.config.store;
+
+/**
+ * the interface configStoreManager provider
+ */
+public interface ConfigStoreManagerProvider {
+ /**
+ * provide a AbstractConfigStoreManager implementation instance
+ * @return ConfigStoreManager
+ */
+ ConfigStoreManager provide();
+}
diff --git a/config/seata-config-core/src/main/java/org/apache/seata/config/store/rocksdb/RocksDBConfigStoreManager.java b/config/seata-config-core/src/main/java/org/apache/seata/config/store/rocksdb/RocksDBConfigStoreManager.java
new file mode 100644
index 00000000000..70ac7ae176c
--- /dev/null
+++ b/config/seata-config-core/src/main/java/org/apache/seata/config/store/rocksdb/RocksDBConfigStoreManager.java
@@ -0,0 +1,691 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.config.store.rocksdb;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+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.ConcurrentMap;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
+
+import org.apache.seata.common.ConfigurationKeys;
+import org.apache.seata.common.util.CollectionUtils;
+import org.apache.seata.common.util.NumberUtils;
+import org.apache.seata.common.util.StringUtils;
+import org.apache.seata.config.Configuration;
+import org.apache.seata.config.ConfigurationChangeEvent;
+import org.apache.seata.config.ConfigurationChangeListener;
+import org.apache.seata.config.ConfigurationFactory;
+import org.apache.seata.config.FileConfiguration;
+import org.apache.seata.config.store.ConfigStoreManager;
+import org.rocksdb.ColumnFamilyDescriptor;
+import org.rocksdb.ColumnFamilyHandle;
+import org.rocksdb.DBOptions;
+import org.rocksdb.Options;
+import org.rocksdb.RocksDB;
+import org.rocksdb.RocksDBException;
+import org.rocksdb.RocksIterator;
+import org.rocksdb.WriteBatch;
+import org.rocksdb.WriteOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.seata.common.ConfigurationKeys.CLIENT_PREFIX;
+import static org.apache.seata.common.ConfigurationKeys.CONFIG_STORE_DATA_ID;
+import static org.apache.seata.common.ConfigurationKeys.CONFIG_STORE_NAMESPACE;
+import static org.apache.seata.common.ConfigurationKeys.FILE_ROOT_PREFIX_CONFIG;
+import static org.apache.seata.common.ConfigurationKeys.FILE_ROOT_PREFIX_REGISTRY;
+import static org.apache.seata.common.ConfigurationKeys.LOG_PREFIX;
+import static org.apache.seata.common.ConfigurationKeys.METRICS_PREFIX;
+import static org.apache.seata.common.ConfigurationKeys.SEATA_FILE_PREFIX_ROOT_CONFIG;
+import static org.apache.seata.common.ConfigurationKeys.SERVER_PREFIX;
+import static org.apache.seata.common.ConfigurationKeys.SERVICE_PREFIX;
+import static org.apache.seata.common.ConfigurationKeys.STORE_PREFIX;
+import static org.apache.seata.common.ConfigurationKeys.TCC_PREFIX;
+import static org.apache.seata.common.ConfigurationKeys.TRANSPORT_PREFIX;
+import static org.apache.seata.common.Constants.DEFAULT_STORE_DATA_ID;
+import static org.apache.seata.common.Constants.DEFAULT_STORE_NAMESPACE;
+
+/**
+ * The RocksDB config store manager
+ *
+ */
+public class RocksDBConfigStoreManager implements ConfigStoreManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(RocksDBConfigStoreManager.class);
+ private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
+ private static final String DB_PATH = RocksDBOptionsFactory.getDBPath();
+ private static final String DEFAULT_NAMESPACE = DEFAULT_STORE_NAMESPACE;
+ private static final String DEFAULT_DATA_ID = DEFAULT_STORE_DATA_ID;
+ private static String CURRENT_DATA_ID;
+ private static String CURRENT_NAMESPACE;
+ private static final String NAME_KEY = "name";
+ private static final String FILE_TYPE = "file";
+ private static final Configuration FILE_CONFIG = ConfigurationFactory.CURRENT_FILE_INSTANCE;
+ private static final DBOptions DB_OPTIONS = RocksDBOptionsFactory.getDBOptions();
+ private static final Map LOCK_MAP = new ConcurrentHashMap<>();
+ private static final int MAP_INITIAL_CAPACITY = 8;
+ private static final ConcurrentMap>> CONFIG_LISTENERS_MAP = new ConcurrentHashMap<>(
+ MAP_INITIAL_CAPACITY);
+
+ //====================================NON COMMON FILED===================================
+ private static volatile RocksDBConfigStoreManager instance;
+ private RocksDB rocksdb;
+ private final Map columnFamilyHandleMap = new ConcurrentHashMap<>();
+ private static final String VERSION_COLUMN_FAMILY = "config_version";
+ private static final List PREFIX_LIST = Arrays.asList(FILE_ROOT_PREFIX_CONFIG, FILE_ROOT_PREFIX_REGISTRY, SERVER_PREFIX, CLIENT_PREFIX, SERVICE_PREFIX,
+ STORE_PREFIX, METRICS_PREFIX, TRANSPORT_PREFIX, LOG_PREFIX, TCC_PREFIX);
+
+
+ public static RocksDBConfigStoreManager getInstance() {
+ if (instance == null) {
+ synchronized (RocksDBConfigStoreManager.class) {
+ if (instance == null) {
+ instance = new RocksDBConfigStoreManager();
+ }
+ }
+ }
+ return instance;
+ }
+
+ public RocksDBConfigStoreManager() {
+ super();
+ CURRENT_NAMESPACE = FILE_CONFIG.getConfig(CONFIG_STORE_NAMESPACE, DEFAULT_NAMESPACE);
+ CURRENT_DATA_ID = FILE_CONFIG.getConfig(CONFIG_STORE_DATA_ID, DEFAULT_DATA_ID);
+ openRocksDB();
+ maybeNeedLoadOriginConfig();
+ LOGGER.info("RocksDBConfigStoreManager initialized successfully");
+ }
+
+ private void openRocksDB() {
+ final List handles = new ArrayList<>();
+ final List descriptors = new ArrayList<>();
+ try (final Options options = new Options()) {
+ List cfs = RocksDB.listColumnFamilies(options, DB_PATH);
+ for (byte[] cf : cfs) {
+ String namespace = new String(cf);
+ descriptors.add(new ColumnFamilyDescriptor(cf, RocksDBOptionsFactory.getColumnFamilyOptionsMap(namespace)));
+ }
+ // create default column family and config version column family
+ if (CollectionUtils.isEmpty(descriptors)) {
+ descriptors.add(new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, RocksDBOptionsFactory.getColumnFamilyOptionsMap(new String(RocksDB.DEFAULT_COLUMN_FAMILY))));
+ descriptors.add(new ColumnFamilyDescriptor(VERSION_COLUMN_FAMILY.getBytes(DEFAULT_CHARSET), RocksDBOptionsFactory.getColumnFamilyOptionsMap(VERSION_COLUMN_FAMILY)));
+ }
+ this.rocksdb = RocksDBFactory.getInstance(DB_PATH, DB_OPTIONS, descriptors, handles);
+ for (ColumnFamilyHandle handle : handles) {
+ columnFamilyHandleMap.put(new String(handle.getName()), handle);
+ }
+ } catch (RocksDBException e) {
+ LOGGER.error("open rocksdb error", e);
+ }
+ }
+
+ private ColumnFamilyHandle getOrCreateColumnFamilyHandle(String namespace) throws RocksDBException {
+ ColumnFamilyHandle handle = columnFamilyHandleMap.get(namespace);
+ if (handle == null) {
+ synchronized (RocksDBConfigStoreManager.class) {
+ handle = columnFamilyHandleMap.get(namespace);
+ if (handle == null) {
+ handle = rocksdb.createColumnFamily(new ColumnFamilyDescriptor(
+ namespace.getBytes(DEFAULT_CHARSET), RocksDBOptionsFactory.getColumnFamilyOptionsMap(namespace)));
+ columnFamilyHandleMap.put(namespace, handle);
+ }
+ }
+ }
+ return handle;
+ }
+
+ /**
+ * load origin config if first startup
+ */
+ private void maybeNeedLoadOriginConfig() {
+ if (isEmpty(CURRENT_NAMESPACE, CURRENT_DATA_ID)) {
+ Map configs = new HashMap<>();
+ Map seataConfigs = new HashMap<>();
+ String pathDataId = String.join(ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR,
+ ConfigurationKeys.FILE_ROOT_CONFIG, FILE_TYPE, NAME_KEY);
+ String name = FILE_CONFIG.getConfig(pathDataId);
+ // create FileConfiguration for read file.conf
+ Optional originFileInstance = Optional.ofNullable(new FileConfiguration(name));
+ originFileInstance
+ .ifPresent(fileConfiguration -> configs.putAll(fileConfiguration.getFileConfig().getAllConfig()));
+ configs.forEach((k, v) -> {
+ if (v instanceof String) {
+ if (StringUtils.isEmpty((String)v)) {
+ return;
+ }
+ }
+ // compatible with the config under Spring Boot
+ if (k.startsWith(SEATA_FILE_PREFIX_ROOT_CONFIG)) {
+ k = k.substring(SEATA_FILE_PREFIX_ROOT_CONFIG.length());
+ }
+ // filter all seata related configs
+ if (PREFIX_LIST.stream().anyMatch(k::startsWith)) {
+ seataConfigs.put(k, v);
+ }
+ });
+ putAll(CURRENT_NAMESPACE, CURRENT_DATA_ID, seataConfigs);
+ LOGGER.info("Load initialization configuration file sucessfully in namespace: {}, dataId: {}", CURRENT_NAMESPACE, CURRENT_DATA_ID);
+ }
+ }
+
+ /**
+ * Acquire lock of the given namespace
+ * @param namespace
+ */
+ private ReentrantReadWriteLock acquireLock(String namespace) {
+ return LOCK_MAP.computeIfAbsent(namespace, k -> new ReentrantReadWriteLock());
+ }
+
+ /**
+ * Get config map of the given namespace and dataId
+ * @param namespace
+ * @param dataId
+ * @return
+ * @throws RocksDBException
+ */
+ private Map getConfigMap(String namespace, String dataId) throws RocksDBException {
+ ReentrantReadWriteLock lock = acquireLock(namespace);
+ lock.readLock().lock();
+ try {
+ ColumnFamilyHandle handle = getOrCreateColumnFamilyHandle(namespace);
+ // the column family not exist, return empty map
+ if (handle == null) {
+ return new HashMap<>();
+ }
+ byte[] value = rocksdb.get(handle, dataId.getBytes(DEFAULT_CHARSET));
+ String configStr = value != null ? new String(value, DEFAULT_CHARSET) : null;
+ return ConfigStoreManager.convertConfigStr2Map(configStr);
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Get the config value of the given namespace and dataId
+ * @param namespace
+ * @param dataId
+ * @param key
+ * @return
+ */
+ @Override
+ public String get(String namespace, String dataId, String key) {
+ ReentrantReadWriteLock lock = acquireLock(namespace);
+ lock.readLock().lock();
+ try {
+ Map configMap = getConfigMap(namespace, dataId);
+ return configMap.get(key) != null ? configMap.get(key).toString() : null;
+ } catch (RocksDBException e) {
+ LOGGER.error("Failed to get value for key: " + key, e);
+ } finally {
+ lock.readLock().unlock();
+ }
+ return null;
+ }
+
+ /**
+ * Get all config items of the given namespace and dataId
+ * @param namespace
+ * @param dataId
+ * @return
+ */
+ @Override
+ public Map getAll(String namespace, String dataId) {
+ try {
+ return getConfigMap(namespace, dataId);
+ } catch (RocksDBException e) {
+ LOGGER.error("Failed to get all configs", e);
+ }
+ return null;
+ }
+
+ /**
+ * Put a config item to the given namespace and dataId
+ * @param namespace
+ * @param dataId
+ * @param key
+ * @param value
+ * @return
+ */
+ @Override
+ public Boolean put(String namespace, String dataId, String key, Object value) {
+ ReentrantReadWriteLock lock = acquireLock(namespace);
+ lock.writeLock().lock();
+ try {
+ Map configMap = getConfigMap(namespace, dataId);
+ configMap.put(key, value);
+ String configStr = ConfigStoreManager.convertConfig2Str(configMap);
+ ColumnFamilyHandle handle = getOrCreateColumnFamilyHandle(namespace);
+ rocksdb.put(handle, dataId.getBytes(DEFAULT_CHARSET), configStr.getBytes(DEFAULT_CHARSET));
+ updateConfigVersion(namespace, dataId);
+ notifyConfigChange(namespace, dataId, new ConfigurationChangeEvent(namespace, dataId, configStr));
+ return true;
+ } catch (RocksDBException e) {
+ LOGGER.error("Failed to put value for key: " + key, e);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Delete a config item with the given key from the given namespace and dataId
+ * @param namespace
+ * @param dataId
+ * @param key
+ * @return
+ */
+ @Override
+ public Boolean delete(String namespace, String dataId, String key) {
+ ReentrantReadWriteLock lock = acquireLock(namespace);
+ lock.writeLock().lock();
+ try {
+ Map configMap = getConfigMap(namespace, dataId);
+ configMap.remove(key);
+ String configStr = ConfigStoreManager.convertConfig2Str(configMap);
+ ColumnFamilyHandle handle = getOrCreateColumnFamilyHandle(namespace);
+ rocksdb.put(handle, dataId.getBytes(DEFAULT_CHARSET), configStr.getBytes(DEFAULT_CHARSET));
+ updateConfigVersion(namespace, dataId);
+ notifyConfigChange(namespace, dataId, new ConfigurationChangeEvent(namespace, dataId, configStr));
+ return true;
+ } catch (RocksDBException e) {
+ LOGGER.error("Failed to delete value for key: " + key, e);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Put all config items into the given namespace and dataId
+ * @param namespace
+ * @param dataId
+ * @param configMap
+ * @return
+ */
+ @Override
+ public Boolean putAll(String namespace, String dataId, Map configMap) {
+ ReentrantReadWriteLock lock = acquireLock(namespace);
+ lock.writeLock().lock();
+ try {
+ String configStr = ConfigStoreManager.convertConfig2Str(configMap);
+ ColumnFamilyHandle handle = getOrCreateColumnFamilyHandle(namespace);
+ rocksdb.put(handle, dataId.getBytes(DEFAULT_CHARSET), configStr.getBytes(DEFAULT_CHARSET));
+ updateConfigVersion(namespace, dataId);
+ notifyConfigChange(namespace, dataId, new ConfigurationChangeEvent(namespace, dataId, configStr));
+ return true;
+ } catch (RocksDBException e) {
+ LOGGER.error("Failed to put all configs", e);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Delete all config items in the given namespace and dataId
+ * @param namespace
+ * @param dataId
+ * @return
+ */
+ @Override
+ public Boolean deleteAll(String namespace, String dataId) {
+ ReentrantReadWriteLock lock = acquireLock(namespace);
+ lock.writeLock().lock();
+ try {
+ ColumnFamilyHandle handle = getOrCreateColumnFamilyHandle(namespace);
+ rocksdb.delete(handle, dataId.getBytes(DEFAULT_CHARSET));
+ deleteConfigVersion(namespace, dataId);
+ notifyConfigChange(namespace, dataId, new ConfigurationChangeEvent(namespace, dataId, null));
+ return true;
+ } catch (RocksDBException e) {
+ LOGGER.error("Failed to clear all configs", e);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ return false;
+ }
+
+
+ /**
+ * Get all key-values pairs in all namespaces, mainly used for backup or snapshot
+ * @return Map(namespace -> Map(dataId -> value))
+ */
+ @Override
+ public Map> getConfigMap() {
+ Map> configMap = new HashMap<>();
+ for (String namespace : columnFamilyHandleMap.keySet()) {
+ HashMap configs = new HashMap<>();
+ ReentrantReadWriteLock lock = acquireLock(namespace);
+ lock.readLock().lock();
+ RocksIterator iterator = null;
+ try {
+ ColumnFamilyHandle handle = getOrCreateColumnFamilyHandle(namespace);
+ iterator = rocksdb.newIterator(handle);
+ for (iterator.seekToFirst(); iterator.isValid(); iterator.next()) {
+ String key = new String(iterator.key(), DEFAULT_CHARSET);
+ String value = new String(iterator.value(), DEFAULT_CHARSET);
+ configs.put(key, value);
+ }
+ configMap.put(namespace, configs);
+ } catch (RocksDBException e) {
+ LOGGER.error("Failed to get configMap in namespace : {}", namespace, e);
+ } finally {
+ if (iterator != null) {
+ iterator.close();
+ }
+ lock.readLock().unlock();
+ }
+ }
+ return configMap;
+ }
+
+ /**
+ * Put all key-value pairs into the specified column family, mainly used for backup or snapshot
+ * @param configMap Map(namespace -> Map(dataId -> value))
+ * @return
+ */
+ @Override
+ public Boolean putConfigMap(Map> configMap) {
+ try (WriteBatch batch = new WriteBatch(); WriteOptions writeOptions = new WriteOptions()) {
+ for (Map.Entry> entry : configMap.entrySet()) {
+ String namespace = entry.getKey();
+ Map configs = entry.getValue();
+ ReentrantReadWriteLock lock = acquireLock(namespace);
+ lock.writeLock().lock();
+ try {
+ ColumnFamilyHandle handle = getOrCreateColumnFamilyHandle(namespace);
+ for (Map.Entry nsEntry : configs .entrySet()) {
+ batch.put(handle, nsEntry.getKey().getBytes(DEFAULT_CHARSET), nsEntry.getValue().toString().getBytes(DEFAULT_CHARSET));
+ }
+ } catch (RocksDBException e) {
+ LOGGER.error("Failed to put configMap in namespace : {}", namespace, e);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+ rocksdb.write(writeOptions, batch);
+ for (Map.Entry> entry : configMap.entrySet()) {
+ String namespace = entry.getKey();
+ Map configs = entry.getValue();
+ for (Map.Entry kv : configs.entrySet()) {
+ String dataId = kv.getKey();
+ updateConfigVersion(namespace, dataId);
+ notifyConfigChange(namespace, dataId, new ConfigurationChangeEvent(namespace, kv.getKey(), kv.getValue().toString()));
+ }
+ }
+ return true;
+ } catch (RocksDBException e) {
+ LOGGER.error("Failed to put all configMap", e);
+ return false;
+ }
+ }
+
+ /**
+ * Empty all data in rocksdb, i.e. delete all key-value pairs in all column family
+ * @return
+ */
+ @Override
+ public Boolean clearData() {
+ Map> clearDataMap = new HashMap<>();
+ try (WriteBatch batch = new WriteBatch(); WriteOptions writeOptions = new WriteOptions()) {
+ for (ColumnFamilyHandle handle : columnFamilyHandleMap.values()) {
+ String namespace = new String(handle.getName());
+ ReentrantReadWriteLock lock = acquireLock(namespace);
+ lock.writeLock().lock();
+ HashSet deleteKeySet = new HashSet<>();
+ try (RocksIterator iterator = rocksdb.newIterator(handle)) {
+ for (iterator.seekToFirst(); iterator.isValid(); iterator.next()) {
+ batch.delete(handle, iterator.key());
+ deleteKeySet.add(new String(iterator.key()));
+ }
+ clearDataMap.put(namespace, deleteKeySet);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+ rocksdb.write(writeOptions, batch);
+ for (Map.Entry> entry : clearDataMap.entrySet()) {
+ String namespace = entry.getKey();
+ for (String key : entry.getValue()) {
+ deleteConfigVersion(namespace, key);
+ notifyConfigChange(namespace, key, new ConfigurationChangeEvent(namespace, key, null));
+ }
+ }
+ return true;
+ } catch (RocksDBException e) {
+ LOGGER.error("Failed to clear all data in rocksdb", e);
+ return false;
+ }
+ }
+
+ /**
+ * Check whether the config data exists in the given namespace and dataId
+ * @param namespace
+ * @param dataId
+ * @return
+ */
+ @Override
+ public Boolean isEmpty(String namespace, String dataId) {
+ return CollectionUtils.isEmpty(getAll(namespace, dataId));
+ }
+
+ /**
+ * Get all namespaces in current rocksdb instance
+ * @return
+ */
+ @Override
+ public List getAllNamespaces() {
+ return columnFamilyHandleMap.keySet().stream()
+ .filter(namespace -> !VERSION_COLUMN_FAMILY.equals(namespace))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Get all dataIds in the given namespace
+ * @param namespace
+ * @return
+ */
+ @Override
+ public List getAllDataIds(String namespace) {
+ if (StringUtils.isEmpty(namespace) || !columnFamilyHandleMap.containsKey(namespace)) {
+ return Collections.emptyList();
+ }
+ List dataIds = new ArrayList<>();
+ ReentrantReadWriteLock lock = acquireLock(namespace);
+ lock.readLock().lock();
+ RocksIterator iterator = null;
+ try {
+ ColumnFamilyHandle handle = getOrCreateColumnFamilyHandle(namespace);
+ iterator = rocksdb.newIterator(handle);
+ for (iterator.seekToFirst(); iterator.isValid(); iterator.next()) {
+ String dataId = new String(iterator.key(), DEFAULT_CHARSET);
+ dataIds.add(dataId);
+ }
+ } catch (RocksDBException e) {
+ LOGGER.error("Failed to get all dataIds in namespace: {}", namespace, e);
+ } finally {
+ if (iterator != null) {
+ iterator.close();
+ }
+ lock.readLock().unlock();
+ }
+ return dataIds;
+ }
+
+ /**
+ * Get the config version in the given namespace and dataId
+ * @param namespace
+ * @param dataId
+ * @return
+ */
+ @Override
+ public Long getConfigVersion(String namespace, String dataId) {
+ ReentrantReadWriteLock lock = acquireLock(VERSION_COLUMN_FAMILY);
+ lock.readLock().lock();
+ try {
+ String configVersionKey = getConfigVersionKey(namespace, dataId);
+ ColumnFamilyHandle handle = getOrCreateColumnFamilyHandle(VERSION_COLUMN_FAMILY);
+ byte[] value = rocksdb.get(handle, configVersionKey.getBytes(DEFAULT_CHARSET));
+ return value != null ? NumberUtils.bytesToLong(value) : null;
+ } catch (RocksDBException | IllegalArgumentException e) {
+ LOGGER.error("Failed to get config version in namespace: {} and dataId: {}", namespace, dataId, e);
+ } finally {
+ lock.readLock().unlock();
+ }
+ return null;
+ }
+
+ /**
+ * Put the config version in the given namespace and dataId
+ * @param namespace
+ * @param dataId
+ * @param version
+ * @return
+ */
+ @Override
+ public Boolean putConfigVersion(String namespace, String dataId, Long version) {
+ ReentrantReadWriteLock lock = acquireLock(VERSION_COLUMN_FAMILY);
+ lock.writeLock().lock();
+ try {
+ String configVersionKey = getConfigVersionKey(namespace, dataId);
+ ColumnFamilyHandle handle = getOrCreateColumnFamilyHandle(VERSION_COLUMN_FAMILY);
+ rocksdb.put(handle, configVersionKey.getBytes(DEFAULT_CHARSET), NumberUtils.longToBytes(version));
+ return true;
+ } catch (RocksDBException | IllegalArgumentException e) {
+ LOGGER.error("Failed to put config version in namespace: {} and dataId: {}", namespace, dataId, e);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Delete the config version in the given namespace and dataId when the config data is deleted.
+ * @param namespace
+ * @param dataId
+ * @return
+ */
+ @Override
+ public Boolean deleteConfigVersion(String namespace, String dataId) {
+ ReentrantReadWriteLock lock = acquireLock(VERSION_COLUMN_FAMILY);
+ lock.writeLock().lock();
+ try {
+ String configVersionKey = getConfigVersionKey(namespace, dataId);
+ ColumnFamilyHandle handle = getOrCreateColumnFamilyHandle(VERSION_COLUMN_FAMILY);
+ rocksdb.delete(handle, configVersionKey.getBytes(DEFAULT_CHARSET));
+ return true;
+ } catch (RocksDBException | IllegalArgumentException e) {
+ LOGGER.error("Failed to put config version in namespace: {} and dataId: {}", namespace, dataId, e);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ return false;
+ }
+
+ private String getConfigVersionKey(String namespace, String dataId) {
+ if (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(dataId)) {
+ throw new IllegalArgumentException("Invalid config namespace or dataId");
+ }
+ return namespace + "_" + dataId;
+ }
+
+ @Override
+ public void shutdown() {
+ synchronized (RocksDBConfigStoreManager.class) {
+ // 1. close all handles
+ for (ColumnFamilyHandle handle : columnFamilyHandleMap.values()) {
+ if (handle != null) {
+ handle.close();
+ }
+ }
+ // 2. close options
+ RocksDBOptionsFactory.releaseAllOptions();
+ // 3. close db
+ RocksDBFactory.close();
+ // 4. destroy db if needed
+ if (RocksDBOptionsFactory.getDBDestroyOnShutdown()) {
+ destroy();
+ }
+ // 5. help gc
+ columnFamilyHandleMap.clear();
+ this.rocksdb = null;
+ LOGGER.info("RocksDBConfigStoreManager has shutdown");
+ }
+ }
+
+ @Override
+ public void destroy() {
+ RocksDBFactory.destroy(DB_PATH);
+ LOGGER.info("DB destroyed, the db path is: {}.", DB_PATH);
+ }
+
+
+ @Override
+ public void addConfigListener(String namespace, String dataId, ConfigurationChangeListener listener) {
+ if (StringUtils.isBlank(namespace) || StringUtils.isBlank(dataId) || listener == null) {
+ return;
+ }
+ Map> listenerMap = CONFIG_LISTENERS_MAP.computeIfAbsent(namespace, k -> new ConcurrentHashMap<>());
+ listenerMap.computeIfAbsent(dataId, k -> ConcurrentHashMap.newKeySet())
+ .add(listener);
+ }
+
+ @Override
+ public void removeConfigListener(String namespace, String dataId, ConfigurationChangeListener listener) {
+ if (StringUtils.isBlank(namespace) || StringUtils.isBlank(dataId) || listener == null) {
+ return;
+ }
+ // dataId -> listener
+ Map> listenerMap = CONFIG_LISTENERS_MAP.get(namespace);
+ if (CollectionUtils.isNotEmpty(listenerMap)) {
+ Set configChangeListeners = listenerMap.get(dataId);
+ if (CollectionUtils.isNotEmpty(configChangeListeners)) {
+ configChangeListeners.remove(listener);
+ }
+ }
+ }
+
+ private void updateConfigVersion(String namespace, String dataId) {
+ Long version = getConfigVersion(namespace, dataId);
+ if (version == null) {
+ version = 0L;
+ }
+ putConfigVersion(namespace, dataId, version + 1);
+ }
+
+
+ private void notifyConfigChange(String namespace, String dataId, ConfigurationChangeEvent event) {
+ Map> listenerMap = CONFIG_LISTENERS_MAP.get(namespace);
+ if (CollectionUtils.isNotEmpty(listenerMap)) {
+ Set configChangeListeners = listenerMap.get(dataId);
+ if (CollectionUtils.isNotEmpty(configChangeListeners)) {
+ configChangeListeners.forEach(listener -> listener.onChangeEvent(event));
+ }
+ }
+ }
+}
diff --git a/config/seata-config-core/src/main/java/org/apache/seata/config/store/rocksdb/RocksDBConfigStoreManagerProvider.java b/config/seata-config-core/src/main/java/org/apache/seata/config/store/rocksdb/RocksDBConfigStoreManagerProvider.java
new file mode 100644
index 00000000000..b9b32a05992
--- /dev/null
+++ b/config/seata-config-core/src/main/java/org/apache/seata/config/store/rocksdb/RocksDBConfigStoreManagerProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.config.store.rocksdb;
+
+import org.apache.seata.common.loader.LoadLevel;
+import org.apache.seata.config.store.ConfigStoreManager;
+import org.apache.seata.config.store.ConfigStoreManagerProvider;
+
+
+@LoadLevel(name = "Rocksdb", order = 1)
+public class RocksDBConfigStoreManagerProvider implements ConfigStoreManagerProvider {
+ @Override
+ public ConfigStoreManager provide() {
+ return RocksDBConfigStoreManager.getInstance();
+ }
+}
diff --git a/config/seata-config-core/src/main/java/org/apache/seata/config/store/rocksdb/RocksDBFactory.java b/config/seata-config-core/src/main/java/org/apache/seata/config/store/rocksdb/RocksDBFactory.java
new file mode 100644
index 00000000000..f03ac872334
--- /dev/null
+++ b/config/seata-config-core/src/main/java/org/apache/seata/config/store/rocksdb/RocksDBFactory.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.config.store.rocksdb;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import org.rocksdb.ColumnFamilyDescriptor;
+import org.rocksdb.ColumnFamilyHandle;
+import org.rocksdb.DBOptions;
+import org.rocksdb.RocksDB;
+import org.rocksdb.Options;
+import org.rocksdb.RocksDBException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The RocksDB Factory
+ *
+ */
+public class RocksDBFactory {
+ private static final Logger LOGGER = LoggerFactory.getLogger(RocksDBFactory.class);
+
+ private static volatile RocksDB instance = null;
+
+
+ static {
+ RocksDB.loadLibrary();
+ }
+
+ public static RocksDB getInstance(String dbPath, DBOptions dbOptions, List columnFamilyDescriptors, List columnFamilyHandles) {
+ if (instance == null) {
+ synchronized (RocksDBFactory.class) {
+ if (instance == null) {
+ instance = build(dbPath, dbOptions, columnFamilyDescriptors, columnFamilyHandles);
+ }
+ }
+ }
+ return instance;
+ }
+
+ private static RocksDB build(String dbPath, DBOptions dbOptions, List columnFamilyDescriptors, List columnFamilyHandles) {
+ try {
+ checkPath(dbPath);
+ return RocksDB.open(dbOptions, dbPath, columnFamilyDescriptors, columnFamilyHandles);
+ } catch (RocksDBException | IOException e) {
+ LOGGER.error("RocksDB open error: {}", e.getMessage(), e);
+ return null;
+ }
+ }
+
+
+ public static synchronized void close() {
+ if (instance != null) {
+ instance.close();
+ instance = null;
+ }
+ }
+
+ public static synchronized void destroy(String dbPath) {
+ close();
+ try (final Options opt = new Options()) {
+ RocksDB.destroyDB(dbPath, opt);
+ } catch (RocksDBException e) {
+ LOGGER.error("RocksDB destroy error: {}", e.getMessage(), e);
+ }
+ }
+
+ private static void checkPath(String dbPath) throws IOException {
+ File directory = new File(dbPath);
+ String message;
+ if (directory.exists()) {
+ if (!directory.isDirectory()) {
+ message = "File " + directory + " exists and is not a directory. Unable to create directory.";
+ throw new IOException(message);
+ }
+ } else if (!directory.mkdirs() && !directory.isDirectory()) {
+ message = "Unable to create directory " + directory;
+ throw new IOException(message);
+ }
+ }
+}
diff --git a/config/seata-config-core/src/main/java/org/apache/seata/config/store/rocksdb/RocksDBOptionsFactory.java b/config/seata-config-core/src/main/java/org/apache/seata/config/store/rocksdb/RocksDBOptionsFactory.java
new file mode 100644
index 00000000000..9846a8b98e1
--- /dev/null
+++ b/config/seata-config-core/src/main/java/org/apache/seata/config/store/rocksdb/RocksDBOptionsFactory.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.config.store.rocksdb;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.seata.common.ConfigurationKeys;
+import org.apache.seata.config.Configuration;
+import org.apache.seata.config.ConfigurationFactory;
+import org.rocksdb.ColumnFamilyOptions;
+import org.rocksdb.CompactionStyle;
+import org.rocksdb.CompressionType;
+import org.rocksdb.DBOptions;
+import org.rocksdb.util.SizeUnit;
+
+import static java.io.File.separator;
+import static org.apache.seata.common.ConfigurationKeys.CONFIG_STORE_DESTROY_ON_SHUTDOWN;
+import static org.apache.seata.common.ConfigurationKeys.CONFIG_STORE_DIR;
+import static org.apache.seata.common.DefaultValues.DEFAULT_SEATA_GROUP;
+
+
+/**
+ * The RocksDB options builder
+ *
+ */
+public class RocksDBOptionsFactory {
+ private static final Configuration FILE_CONFIG = ConfigurationFactory.CURRENT_FILE_INSTANCE;
+
+ public static final String ROCKSDB_SUFFIX = "rocksdb";
+ private static volatile DBOptions options = null;
+ private static final Map COLUMN_FAMILY_OPTIONS_MAP = new ConcurrentHashMap<>();
+ public static DBOptions getDBOptions() {
+ if (options == null) {
+ synchronized (RocksDBOptionsFactory.class) {
+ if (options == null) {
+ options = buildDBOptions();
+ }
+ }
+ }
+ return options;
+ }
+
+ public static ColumnFamilyOptions getColumnFamilyOptionsMap(final String namespace) {
+ ColumnFamilyOptions opts = COLUMN_FAMILY_OPTIONS_MAP.get(namespace);
+ if (opts == null) {
+ final ColumnFamilyOptions newOpts = buildColumnFamilyOptions();
+ opts = COLUMN_FAMILY_OPTIONS_MAP.putIfAbsent(namespace, newOpts);
+ if (opts != null) {
+ newOpts.close();
+ } else {
+ opts = newOpts;
+ }
+ }
+ return opts;
+ }
+ public static String getDBPath() {
+ String dir = FILE_CONFIG.getConfig(CONFIG_STORE_DIR);
+ String group = FILE_CONFIG.getConfig(ConfigurationKeys.SERVER_RAFT_GROUP, DEFAULT_SEATA_GROUP);
+ return String.join(separator, dir, group, ROCKSDB_SUFFIX);
+ }
+
+ public static boolean getDBDestroyOnShutdown() {
+ return FILE_CONFIG.getBoolean(CONFIG_STORE_DESTROY_ON_SHUTDOWN, false);
+ }
+
+ private static DBOptions buildDBOptions() {
+ final DBOptions options = new DBOptions();
+ // If the database does not exist, create it
+ options.setCreateIfMissing(true);
+ // If true, missing column families will be automatically created.
+ options.setCreateMissingColumnFamilies(true);
+ // Retain only the latest log file
+ options.setKeepLogFileNum(1);
+ // Disable log file rolling based on time
+ options.setLogFileTimeToRoll(0);
+ // Disable log file rolling based on size
+ options.setMaxLogFileSize(0);
+ // Number of open files that can be used by the DB.
+ options.setMaxOpenFiles(-1);
+ return options;
+ }
+
+ private static ColumnFamilyOptions buildColumnFamilyOptions() {
+ ColumnFamilyOptions columnFamilyOptions = new ColumnFamilyOptions();
+ // set little write buffer size, since the size of config file is small
+ columnFamilyOptions.setWriteBufferSize(4 * SizeUnit.MB);
+ // Set memtable prefix bloom filter to reduce memory usage.
+ columnFamilyOptions.setMemtablePrefixBloomSizeRatio(0.125);
+ // Set compression type
+ columnFamilyOptions.setCompressionType(CompressionType.LZ4_COMPRESSION);
+ // Set compaction style
+ columnFamilyOptions.setCompactionStyle(CompactionStyle.LEVEL);
+ // Optimize level style compaction
+ columnFamilyOptions.optimizeLevelStyleCompaction();
+ return columnFamilyOptions;
+ }
+
+ public static void releaseAllOptions() {
+ // close all options
+ if (options != null) {
+ options.close();
+ }
+ for (final ColumnFamilyOptions opts : COLUMN_FAMILY_OPTIONS_MAP.values()) {
+ if (opts != null) {
+ opts.close();
+ }
+ }
+ // help gc
+ options = null;
+ COLUMN_FAMILY_OPTIONS_MAP.clear();
+ }
+
+}
diff --git a/config/seata-config-core/src/main/resources/META-INF/services/org.apache.seata.config.store.ConfigStoreManagerProvider b/config/seata-config-core/src/main/resources/META-INF/services/org.apache.seata.config.store.ConfigStoreManagerProvider
new file mode 100644
index 00000000000..0d199fafe7c
--- /dev/null
+++ b/config/seata-config-core/src/main/resources/META-INF/services/org.apache.seata.config.store.ConfigStoreManagerProvider
@@ -0,0 +1,17 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+org.apache.seata.config.store.rocksdb.RocksDBConfigStoreManagerProvider
\ No newline at end of file
diff --git a/config/seata-config-core/src/test/java/org/apache/seata/config/store/rocksdb/RocksDBTest.java b/config/seata-config-core/src/test/java/org/apache/seata/config/store/rocksdb/RocksDBTest.java
new file mode 100644
index 00000000000..6850ebedde5
--- /dev/null
+++ b/config/seata-config-core/src/test/java/org/apache/seata/config/store/rocksdb/RocksDBTest.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.config.store.rocksdb;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.apache.seata.common.Constants.DEFAULT_STORE_DATA_ID;
+import static org.apache.seata.common.Constants.DEFAULT_STORE_NAMESPACE;
+
+
+class RocksDBTest {
+ private static RocksDBConfigStoreManager configStoreManager;
+
+ private static final String dataId = DEFAULT_STORE_DATA_ID;
+ private static final String namespace = DEFAULT_STORE_NAMESPACE;
+ @BeforeAll
+ static void setUp() {
+ configStoreManager = RocksDBConfigStoreManager.getInstance();
+ }
+
+ @AfterAll
+ static void tearDown() {
+ if (configStoreManager != null) {
+ configStoreManager.shutdown();
+ configStoreManager.destroy();
+ }
+ }
+
+ @Test
+ void getConfigStoreManagerTest() {
+ Assertions.assertNotNull(configStoreManager);
+ }
+
+
+ @Test
+ void crudTest() {
+ configStoreManager.deleteAll(namespace, dataId);
+ String key = "aaa";
+ String value = "bbb";
+ String updateValue = "ccc";
+ Assertions.assertTrue(configStoreManager.put(namespace, dataId, key, value));
+ Assertions.assertEquals(value, configStoreManager.get(namespace, dataId, key));
+ Assertions.assertTrue(configStoreManager.put(namespace, dataId, key, updateValue));
+ Assertions.assertEquals(updateValue, configStoreManager.get(namespace, dataId, key));
+ Assertions.assertTrue(configStoreManager.delete(namespace, dataId, key));
+ Assertions.assertNull(configStoreManager.get(namespace, dataId, key));
+
+ }
+
+ @Test
+ void uploadConfigTest() {
+ configStoreManager.deleteAll(namespace, dataId);
+ HashMap uploadConfigs = new HashMap<>();
+ uploadConfigs.put("aaa","111");
+ uploadConfigs.put("bbb","222");
+ Assertions.assertTrue(configStoreManager.putAll(namespace, dataId, uploadConfigs));
+ Assertions.assertEquals(uploadConfigs, configStoreManager.getAll(namespace, dataId));
+ configStoreManager.deleteAll(namespace, dataId);
+ Assertions.assertTrue(configStoreManager.isEmpty(namespace, dataId));
+ }
+
+
+ @Test
+ void multiGroupTest() {
+ configStoreManager.deleteAll(namespace, dataId);
+ String group1 = "group1";
+ String group2 = "group2";
+ String key = "aaa";
+ String value1 = "aaa";
+ String value2 = "bbb";
+ // put and get
+ Assertions.assertTrue(configStoreManager.put(namespace, group1, key, value1));
+ Assertions.assertTrue(configStoreManager.put(namespace, group2, key, value2));
+ Assertions.assertEquals(value1, configStoreManager.get(namespace, group1, key));
+ Assertions.assertEquals(value2, configStoreManager.get(namespace, group2, key));
+
+ // delete
+ Assertions.assertTrue(configStoreManager.delete(namespace, group1, key));
+ Assertions.assertTrue(configStoreManager.delete(namespace, group2, key));
+ Assertions.assertNull(configStoreManager.get(namespace, group1, key));
+ Assertions.assertNull(configStoreManager.get(namespace, group2, key));
+ }
+
+
+ @Test
+ void multiNamespaceAndGroupTest() {
+ configStoreManager.clearData();
+ String namespace1 = "namespace1";
+ String namespace2 = "namespace2";
+ List namespaces = Arrays.asList(DEFAULT_STORE_NAMESPACE, namespace1, namespace2);
+ String dataId1 = "dataId1";
+ String dataId2 = "dataId2";
+ List dataIds = Arrays.asList(dataId1, dataId2);
+ String key = "aaa";
+ // put and get
+ Assertions.assertTrue(configStoreManager.put(namespace1, dataId1, key , "11"));
+ Assertions.assertTrue(configStoreManager.put(namespace1, dataId2, key , "12"));
+ Assertions.assertTrue(configStoreManager.put(namespace2, dataId1, key , "21"));
+ Assertions.assertTrue(configStoreManager.put(namespace2, dataId2, key , "22"));
+ Assertions.assertEquals("11", configStoreManager.get(namespace1, dataId1, key));
+ Assertions.assertEquals("12", configStoreManager.get(namespace1, dataId2, key));
+ Assertions.assertEquals("21", configStoreManager.get(namespace2, dataId1, key));
+ Assertions.assertEquals("22", configStoreManager.get(namespace2, dataId2, key));
+ Assertions.assertEquals(namespaces.size(), configStoreManager.getAllNamespaces().size());
+ Assertions.assertEquals(dataIds.size(), configStoreManager.getAllDataIds(namespace1).size());
+ Assertions.assertEquals(dataIds.size(), configStoreManager.getAllDataIds(namespace2).size());
+ // delete
+ Assertions.assertTrue(configStoreManager.delete(namespace1, dataId1, key));
+ Assertions.assertTrue(configStoreManager.delete(namespace1, dataId2, key));
+ Assertions.assertTrue(configStoreManager.delete(namespace2, dataId1, key));
+ Assertions.assertTrue(configStoreManager.delete(namespace2, dataId2, key));
+ Assertions.assertNull(configStoreManager.get(namespace1, dataId1, key));
+ Assertions.assertNull(configStoreManager.get(namespace1, dataId2, key));
+ Assertions.assertNull(configStoreManager.get(namespace2, dataId1, key));
+ Assertions.assertNull(configStoreManager.get(namespace2, dataId2, key));
+ }
+
+ @Test
+ void uploadTest() {
+ configStoreManager.clearData();
+ String namespace1 = "namespace1";
+ String namespace2 = "namespace2";
+ String dataId1 = "dataId1";
+ String dataId2 = "dataId2";
+ HashMap> configMap = new HashMap>();
+ HashMap map1 = new HashMap() {{
+ put(dataId1, "11");
+ put(dataId2, "12");
+ }};
+ HashMap map2 = new HashMap() {{
+ put(dataId1, "21");
+ put(dataId2, "22");
+ }};
+ configMap.put(namespace1,map1);
+ configMap.put(namespace2,map2);
+ // ensure default namespace
+ configMap.put("default",new HashMap<>());
+ Assertions.assertTrue(configStoreManager.putConfigMap(configMap));
+ Map> other = configStoreManager.getConfigMap();
+
+ Assertions.assertEquals(configMap.get(namespace1), other.get(namespace1));
+ Assertions.assertEquals(configMap.get(namespace2), other.get(namespace2));
+ Assertions.assertEquals(configMap.get("default"), other.get("default"));
+
+ Assertions.assertDoesNotThrow(()->configStoreManager.getAll(namespace1, dataId1));
+ }
+
+ @Test
+ void configVersionTest() {
+ configStoreManager.clearData();
+ Long version = 0L;
+
+ String key = "aaa";
+ String value = "bbb";
+ String newValue = "ccc";
+
+ Assertions.assertTrue(configStoreManager.put(namespace, dataId, key, value));
+ version++;
+ Assertions.assertEquals(version, configStoreManager.getConfigVersion(namespace, dataId));
+
+ Assertions.assertTrue(configStoreManager.put(namespace, dataId, key, newValue));
+ version++;
+ Assertions.assertEquals(version, configStoreManager.getConfigVersion(namespace, dataId));
+
+ Assertions.assertTrue(configStoreManager.deleteAll(namespace, dataId));
+ Assertions.assertNull(configStoreManager.getConfigVersion(namespace, dataId));
+ }
+}
diff --git a/config/seata-config-core/src/test/resources/registry.conf b/config/seata-config-core/src/test/resources/registry.conf
index bab6e8ec0ef..343179ab6a8 100644
--- a/config/seata-config-core/src/test/resources/registry.conf
+++ b/config/seata-config-core/src/test/resources/registry.conf
@@ -87,4 +87,14 @@ config {
file {
name = "file.conf"
}
+ raft {
+ db {
+ type = "rocksdb"
+ dir = "configStore"
+ destroy-on-shutdown = false
+ namespace = "default"
+ dataId = "seata.properties"
+ }
+ }
+
}
diff --git a/config/seata-config-raft/pom.xml b/config/seata-config-raft/pom.xml
new file mode 100644
index 00000000000..20b7b3ddb5c
--- /dev/null
+++ b/config/seata-config-raft/pom.xml
@@ -0,0 +1,44 @@
+
+
+
+
+ org.apache.seata
+ seata-config
+ ${revision}
+
+ 4.0.0
+ seata-config-raft
+ seata-config-raft ${project.version}
+ config-raft for Seata built with Maven
+
+
+
+ org.apache.seata
+ seata-config-core
+ ${project.version}
+
+
+ org.apache.httpcomponents
+ httpclient
+
+
+
+
\ No newline at end of file
diff --git a/config/seata-config-raft/src/main/java/org/apache/seata/config/raft/RaftConfigurationClient.java b/config/seata-config-raft/src/main/java/org/apache/seata/config/raft/RaftConfigurationClient.java
new file mode 100644
index 00000000000..c27626f9eec
--- /dev/null
+++ b/config/seata-config-raft/src/main/java/org/apache/seata/config/raft/RaftConfigurationClient.java
@@ -0,0 +1,694 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.config.raft;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.http.HttpStatus;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.entity.ContentType;
+import org.apache.http.protocol.HTTP;
+import org.apache.http.util.EntityUtils;
+import org.apache.seata.common.ConfigurationKeys;
+import org.apache.seata.common.config.ConfigDataResponse;
+import org.apache.seata.common.exception.AuthenticationFailedException;
+import org.apache.seata.common.exception.ErrorCode;
+import org.apache.seata.common.exception.NotSupportYetException;
+import org.apache.seata.common.exception.RetryableException;
+import org.apache.seata.common.exception.SeataRuntimeException;
+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.thread.NamedThreadFactory;
+import org.apache.seata.common.util.CollectionUtils;
+import org.apache.seata.common.util.HttpClientUtil;
+import org.apache.seata.common.util.StringUtils;
+import org.apache.seata.config.AbstractConfiguration;
+import org.apache.seata.config.Configuration;
+import org.apache.seata.config.ConfigurationChangeEvent;
+import org.apache.seata.config.ConfigurationChangeListener;
+import org.apache.seata.config.ConfigurationChangeType;
+import org.apache.seata.config.ConfigurationFactory;
+import org.apache.seata.config.dto.ConfigurationInfoDto;
+import org.apache.seata.config.dto.ConfigurationItem;
+import org.apache.seata.config.store.ConfigStoreManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.seata.common.ConfigurationKeys.CONFIG_STORE_DATA_ID;
+import static org.apache.seata.common.ConfigurationKeys.CONFIG_STORE_NAMESPACE;
+import static org.apache.seata.common.Constants.DEFAULT_STORE_DATA_ID;
+import static org.apache.seata.common.Constants.DEFAULT_STORE_NAMESPACE;
+import static org.apache.seata.common.Constants.RAFT_CONFIG_GROUP;
+import static org.apache.seata.common.DefaultValues.DEFAULT_SEATA_GROUP;
+
+/**
+ * The type Raft configuration of client.
+ *
+ */
+public class RaftConfigurationClient extends AbstractConfiguration {
+ private static final Logger LOGGER = LoggerFactory.getLogger(RaftConfigurationClient.class);
+ private static final String CONFIG_TYPE = "raft";
+ private static final String SERVER_ADDR_KEY = "serverAddr";
+ private static final String RAFT_GROUP = RAFT_CONFIG_GROUP;
+ private static final String RAFT_CLUSTER = DEFAULT_SEATA_GROUP;
+ private static final String CONFIG_NAMESPACE;
+ private static final String CONFIG_DATA_ID;
+ private static final String HTTP_PREFIX = "http://";
+ private static final String USERNAME_KEY = "username";
+ private static final String PASSWORD_KEY = "password";
+ private static final String AUTHORIZATION_HEADER = "Authorization";
+ private static final String TOKEN_VALID_TIME_MS_KEY = "tokenValidityInMilliseconds";
+ private static volatile RaftConfigurationClient instance;
+ private static final Configuration FILE_CONFIG = ConfigurationFactory.CURRENT_FILE_INSTANCE;
+ private static final String USERNAME;
+ private static final String PASSWORD;
+ private static final long TOKEN_EXPIRE_TIME_IN_MILLISECONDS;
+ private static volatile long tokenTimeStamp = -1;
+ private static volatile String jwtToken;
+ private static final String IP_PORT_SPLIT_CHAR = ":";
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ private static final Map> INIT_ADDRESSES = new HashMap<>();
+ private static final Metadata METADATA = new Metadata();
+ private static volatile ThreadPoolExecutor REFRESH_METADATA_EXECUTOR;
+ private static volatile ThreadPoolExecutor REFRESH_CONFIG_EXECUTOR;
+ private static final AtomicBoolean CLOSED = new AtomicBoolean(false);
+ private static final AtomicBoolean CONFIG_CLOSED = new AtomicBoolean(false);
+ private static volatile Properties seataConfig = new Properties();
+ private static final AtomicLong CONFIG_VERSION = new AtomicLong(0);
+ private static final int MAP_INITIAL_CAPACITY = 8;
+ private static final ConcurrentMap> CONFIG_LISTENERS_MAP
+ = new ConcurrentHashMap<>(MAP_INITIAL_CAPACITY);
+
+ private static ConfigStoreListener CONFIG_LISTENER;
+ static {
+ USERNAME = FILE_CONFIG.getConfig(getRaftUsernameKey());
+ PASSWORD = FILE_CONFIG.getConfig(getRaftPasswordKey());
+ TOKEN_EXPIRE_TIME_IN_MILLISECONDS = FILE_CONFIG.getLong(getTokenExpireTimeInMillisecondsKey(), 29 * 60 * 1000L);
+ CONFIG_NAMESPACE = FILE_CONFIG.getConfig(CONFIG_STORE_NAMESPACE, DEFAULT_STORE_NAMESPACE);
+ CONFIG_DATA_ID = FILE_CONFIG.getConfig(CONFIG_STORE_DATA_ID, DEFAULT_STORE_DATA_ID);
+ }
+
+ public static RaftConfigurationClient getInstance() {
+ if (instance == null) {
+ synchronized (RaftConfigurationClient.class) {
+ if (instance == null) {
+ instance = new RaftConfigurationClient();
+ }
+ }
+ }
+ return instance;
+ }
+
+ private RaftConfigurationClient() {
+ initClusterMetaData();
+ initClientConfig();
+ }
+
+ private static void initClientConfig() {
+ try {
+ Map configMap = acquireClusterConfigData(RAFT_CLUSTER, RAFT_GROUP, CONFIG_NAMESPACE, CONFIG_DATA_ID);
+ if (configMap != null) {
+ seataConfig.putAll(configMap);
+ }
+ CONFIG_LISTENER = new ConfigStoreListener(CONFIG_NAMESPACE, null);
+ startQueryConfigData();
+ } catch (RetryableException e) {
+ LOGGER.error("init config properties error:{}", e.getMessage(), e);
+ }
+
+ }
+ private static String queryHttpAddress(String clusterName, String group) {
+ List nodeList = METADATA.getNodes(clusterName, group);
+ List addressList = null;
+ Stream stream = null;
+ if (CollectionUtils.isNotEmpty(nodeList)) {
+ addressList =
+ nodeList.stream().map(node -> node.getControl().createAddress()).collect(Collectors.toList());
+ } else {
+ stream = INIT_ADDRESSES.get(clusterName).stream();
+ }
+ if (addressList != null) {
+ return addressList.get(ThreadLocalRandom.current().nextInt(addressList.size()));
+ } else {
+ Map map = new HashMap<>();
+ if (CollectionUtils.isNotEmpty(nodeList)) {
+ for (Node node : nodeList) {
+ map.put(new InetSocketAddress(node.getTransaction().getHost(), node.getTransaction().getPort()).getAddress().getHostAddress()
+ + IP_PORT_SPLIT_CHAR + node.getTransaction().getPort(), node);
+ }
+ }
+ addressList = stream.map(inetSocketAddress -> {
+ String host = inetSocketAddress.getAddress().getHostAddress();
+ Node node = map.get(host + IP_PORT_SPLIT_CHAR + inetSocketAddress.getPort());
+ return host + IP_PORT_SPLIT_CHAR
+ + (node != null ? node.getControl().getPort() : inetSocketAddress.getPort());
+ }).collect(Collectors.toList());
+ return addressList.get(ThreadLocalRandom.current().nextInt(addressList.size()));
+ }
+ }
+ private static void acquireClusterMetaData(String clusterName, String group) throws RetryableException {
+ String tcAddress = queryHttpAddress(clusterName, group);
+ Map header = new HashMap<>();
+ header.put(HTTP.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
+ if (isTokenExpired()) {
+ refreshToken(tcAddress);
+ }
+ if (StringUtils.isNotBlank(jwtToken)) {
+ header.put(AUTHORIZATION_HEADER, jwtToken);
+ }
+ if (StringUtils.isNotBlank(tcAddress)) {
+ Map param = new HashMap<>();
+ // param.put("group", group);
+ String response = null;
+ try (CloseableHttpResponse httpResponse =
+ HttpClientUtil.doGet(HTTP_PREFIX + tcAddress + "/metadata/v1/config/cluster", param, header, 1000)) {
+ if (httpResponse != null) {
+ if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ response = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8);
+ } else if (httpResponse.getStatusLine().getStatusCode() == 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.");
+ }
+ }
+ }
+ MetadataResponse metadataResponse;
+ if (StringUtils.isNotBlank(response)) {
+ try {
+ metadataResponse = OBJECT_MAPPER.readValue(response, MetadataResponse.class);
+ METADATA.refreshMetadata(clusterName, metadataResponse);
+ } catch (JsonProcessingException e) {
+ LOGGER.error("acquireClusterMetaData:{}", e.getMessage(), e);
+ }
+ }
+ } catch (IOException e) {
+ throw new RetryableException(e.getMessage(), e);
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map acquireClusterConfigData(String clusterName, String group, String configNamespace, String configDataId) throws RetryableException {
+ String tcAddress = queryHttpAddress(clusterName, group);
+ Map header = new HashMap<>();
+ header.put(HTTP.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
+ if (isTokenExpired()) {
+ refreshToken(tcAddress);
+ }
+ if (StringUtils.isNotBlank(jwtToken)) {
+ header.put(AUTHORIZATION_HEADER, jwtToken);
+ }
+ if (StringUtils.isNotBlank(tcAddress)) {
+ Map param = new HashMap<>();
+ param.put("namespace", configNamespace);
+ param.put("dataId", configDataId);
+ String response = null;
+ try (CloseableHttpResponse httpResponse =
+ HttpClientUtil.doGet(HTTP_PREFIX + tcAddress + "/metadata/v1/config/getAll", param, header, 1000)) {
+ if (httpResponse != null) {
+ if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ response = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8);
+ } else if (httpResponse.getStatusLine().getStatusCode() == 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.");
+ }
+ }
+ }
+
+ ConfigDataResponse configDataResponse;
+ if (StringUtils.isNotBlank(response)) {
+ try {
+ configDataResponse = OBJECT_MAPPER.readValue(response, new TypeReference>() {
+ });
+ if (configDataResponse.getSuccess()) {
+ ConfigurationInfoDto configurationInfoDto = configDataResponse.getResult();
+ Map configItemMap = configurationInfoDto.getConfig();
+ Map configMap = configItemMap.entrySet().stream()
+ .collect(Collectors.toMap(
+ Map.Entry::getKey, entry -> entry.getValue().getValue()));
+ Long version = configurationInfoDto.getVersion() == null ? -1 : configurationInfoDto.getVersion();
+ Long currentVersion = CONFIG_VERSION.get();
+ if (version < currentVersion) {
+ LOGGER.info("The configuration version: {} of the server is lower than the current configuration: {} , it may be expired configuration.", version, CONFIG_VERSION.get());
+ throw new RetryableException("Expired configuration!");
+ } else {
+ CONFIG_VERSION.set(version);
+ return configMap;
+ }
+ } else {
+ throw new RetryableException(configDataResponse.getErrMsg());
+ }
+ } catch (JsonProcessingException e) {
+ LOGGER.error("acquireClusterConfigData:{}", e.getMessage(), e);
+ }
+ }
+ } catch (IOException e) {
+ throw new RetryableException(e.getMessage(), e);
+ }
+ }
+ return null;
+ }
+
+ protected static void startQueryMetadata() {
+ if (REFRESH_METADATA_EXECUTOR == null) {
+ synchronized (INIT_ADDRESSES) {
+ if (REFRESH_METADATA_EXECUTOR == null) {
+ REFRESH_METADATA_EXECUTOR = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue<>(), new NamedThreadFactory("refreshMetadata", 1, true));
+ REFRESH_METADATA_EXECUTOR.execute(() -> {
+ long metadataMaxAgeMs = FILE_CONFIG.getLong(ConfigurationKeys.CLIENT_METADATA_MAX_AGE_MS, 30000L);
+ long currentTime = System.currentTimeMillis();
+ while (!CLOSED.get()) {
+ try {
+ // Forced refresh of metadata information after set age
+ boolean fetch = System.currentTimeMillis() - currentTime > metadataMaxAgeMs;
+ String clusterName = RAFT_CLUSTER;
+ 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 {
+ LOGGER.error("failed to get the leader address,error: {}", e.getMessage(), e);
+ }
+ }
+ }
+ currentTime = System.currentTimeMillis();
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("refresh seata cluster metadata time: {}", currentTime);
+ }
+ }
+ } catch (RetryableException e) {
+ LOGGER.error("startQueryMetadata:{}", e.getMessage(), e);
+ try {
+ TimeUnit.MILLISECONDS.sleep(1000);
+ } catch (InterruptedException ignored) {
+ }
+ }
+ }
+ });
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ if (CLOSED.compareAndSet(false, true)) {
+ REFRESH_METADATA_EXECUTOR.shutdown();
+ }
+ }));
+ }
+ }
+ }
+ }
+
+ protected static void startQueryConfigData() {
+ if (REFRESH_CONFIG_EXECUTOR == null) {
+ synchronized (RaftConfigurationClient.class) {
+ if (REFRESH_CONFIG_EXECUTOR == null) {
+ REFRESH_CONFIG_EXECUTOR = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue<>(), new NamedThreadFactory("refreshConfig", 1, true));
+ REFRESH_CONFIG_EXECUTOR.execute(() -> {
+ long metadataMaxAgeMs = FILE_CONFIG.getLong(ConfigurationKeys.CLIENT_METADATA_MAX_AGE_MS, 30000L);
+ long currentTime = System.currentTimeMillis();
+ while (!CONFIG_CLOSED.get()) {
+ try {
+ // Forced refresh of metadata information after set age
+ boolean fetch = System.currentTimeMillis() - currentTime > metadataMaxAgeMs;
+ if (!fetch) {
+ fetch = configWatch();
+ }
+ // Cluster config changes or reaches timeout refresh time
+ if (fetch) {
+ try {
+ Map configMap = acquireClusterConfigData(RAFT_CLUSTER, RAFT_GROUP, CONFIG_NAMESPACE, CONFIG_DATA_ID);
+ if (CollectionUtils.isNotEmpty(configMap)) {
+ notifyConfigMayChange(configMap);
+ }
+ } catch (Exception e) {
+ // prevents an exception from being thrown that causes the thread to break
+ if (e instanceof RetryableException) {
+ throw e;
+ } else {
+ LOGGER.error("failed to get the config ,error: {}", e.getMessage(), e);
+ }
+ }
+
+ currentTime = System.currentTimeMillis();
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("refresh seata cluster config time: {}", currentTime);
+ }
+ }
+ } catch (RetryableException e) {
+ LOGGER.error("startQueryConfigData:{}", e.getMessage(), e);
+ try {
+ TimeUnit.MILLISECONDS.sleep(1000);
+ } catch (InterruptedException ignored) {
+ }
+ }
+ }
+ });
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ if (CONFIG_CLOSED.compareAndSet(false, true)) {
+ REFRESH_CONFIG_EXECUTOR.shutdown();
+ }
+ }));
+ }
+ }
+ }
+ }
+ private static boolean watch() throws RetryableException {
+ Map header = new HashMap<>();
+ header.put(HTTP.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
+ Map param = new HashMap<>();
+ String clusterName = RAFT_CLUSTER;
+ Map 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 (CloseableHttpResponse response =
+ HttpClientUtil.doPost(HTTP_PREFIX + tcAddress + "/metadata/v1/watch", param, header, 30000)) {
+ if (response != null) {
+ StatusLine statusLine = response.getStatusLine();
+ if (statusLine != null && statusLine.getStatusCode() == 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 statusLine != null && statusLine.getStatusCode() == HttpStatus.SC_OK;
+ }
+ } catch (IOException e) {
+ LOGGER.error("watch cluster node: {}, fail: {}", tcAddress, e.getMessage(), e);
+ throw new RetryableException(e.getMessage(), e);
+ }
+ break;
+ }
+ return false;
+ }
+
+ private static boolean configWatch() throws RetryableException {
+ Map header = new HashMap<>();
+ header.put(HTTP.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
+ String tcAddress = queryHttpAddress(RAFT_CLUSTER, RAFT_GROUP);
+ Map param = new HashMap<>();
+ param.put("namespace", CONFIG_NAMESPACE);
+ param.put("dataId", CONFIG_DATA_ID);
+ param.put("version", CONFIG_VERSION.toString());
+ if (isTokenExpired()) {
+ refreshToken(tcAddress);
+ }
+ if (StringUtils.isNotBlank(jwtToken)) {
+ header.put(AUTHORIZATION_HEADER, jwtToken);
+ }
+ try (CloseableHttpResponse response =
+ HttpClientUtil.doPost(HTTP_PREFIX + tcAddress + "/metadata/v1/config/watch", param, header, 30000)) {
+ if (response != null) {
+ StatusLine statusLine = response.getStatusLine();
+ if (statusLine != null && statusLine.getStatusCode() == 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 statusLine != null && statusLine.getStatusCode() == HttpStatus.SC_OK;
+ }
+ } catch (IOException e) {
+ LOGGER.error("watch cluster node: {}, fail: {}", tcAddress, e.getMessage(), e);
+ throw new RetryableException(e.getMessage(), e);
+ }
+ return false;
+ }
+ private static void initClusterMetaData() {
+ String clusterName = RAFT_CLUSTER;
+ String group = RAFT_GROUP;
+ if (!METADATA.containsGroup(clusterName)) {
+ String raftClusterAddress = FILE_CONFIG.getConfig(getRaftServerAddrKey());
+ if (StringUtils.isNotBlank(raftClusterAddress)) {
+ List list = new ArrayList<>();
+ String[] addresses = raftClusterAddress.split(",");
+ for (String address : addresses) {
+ String[] endpoint = address.split(IP_PORT_SPLIT_CHAR);
+ String host = endpoint[0];
+ int port = Integer.parseInt(endpoint[1]);
+ list.add(new InetSocketAddress(host, port));
+ }
+ if (CollectionUtils.isEmpty(list)) {
+ throw new SeataRuntimeException(ErrorCode.ERR_CONFIG,
+ "There are no valid raft addr! you should configure the correct [config.raft.server-addr] in the config file");
+ }
+ INIT_ADDRESSES.put(clusterName, list);
+ // init jwt token
+ try {
+ refreshToken(queryHttpAddress(clusterName, group));
+ } catch (Exception e) {
+ throw new RuntimeException("Init fetch token failed!", e);
+ }
+ // Refresh the metadata by initializing the address
+ try {
+ acquireClusterMetaData(clusterName, group);
+ } catch (RetryableException e) {
+ LOGGER.error("init cluster metadata fail: {}", e.getMessage(), e);
+ }
+ startQueryMetadata();
+ }
+ }
+ }
+
+
+
+ @Override
+ public String getTypeName() {
+ return CONFIG_TYPE;
+ }
+
+ @Override
+ public boolean putConfig(String dataId, String content, long timeoutMills) {
+ throw new NotSupportYetException("not support operation putConfig");
+ }
+
+ @Override
+ public String getLatestConfig(String dataId, String defaultValue, long timeoutMills) {
+ String value = seataConfig.getProperty(dataId);
+ if (value == null) {
+ try {
+ Map configMap = acquireClusterConfigData(RAFT_CLUSTER, RAFT_GROUP, CONFIG_NAMESPACE, CONFIG_DATA_ID);
+ if (CollectionUtils.isNotEmpty(configMap)) {
+ value = configMap.get(dataId) == null ? null : configMap.get(dataId).toString();
+ }
+ } catch (RetryableException e) {
+ LOGGER.error(e.getMessage());
+ }
+ }
+ return value == null ? defaultValue : value;
+ }
+
+ @Override
+ public boolean putConfigIfAbsent(String dataId, String content, long timeoutMills) {
+ throw new NotSupportYetException("not support atomic operation putConfigIfAbsent");
+ }
+
+ @Override
+ public boolean removeConfig(String dataId, long timeoutMills) {
+ throw new NotSupportYetException("not support operation removeConfig");
+ }
+
+ @Override
+ public void addConfigListener(String dataId, ConfigurationChangeListener listener) {
+ if (StringUtils.isBlank(dataId) || listener == null) {
+ return;
+ }
+ ConfigStoreListener storeListener = new ConfigStoreListener(dataId, listener);
+ CONFIG_LISTENERS_MAP.computeIfAbsent(dataId, key -> new ConcurrentHashMap<>())
+ .put(listener, storeListener);
+ }
+
+ @Override
+ public void removeConfigListener(String dataId, ConfigurationChangeListener listener) {
+ if (StringUtils.isBlank(dataId) || listener == null) {
+ return;
+ }
+ Set configChangeListeners = getConfigListeners(dataId);
+ if (CollectionUtils.isNotEmpty(configChangeListeners)) {
+ for (ConfigurationChangeListener entry : configChangeListeners) {
+ if (listener.equals(entry)) {
+ Map configListeners = CONFIG_LISTENERS_MAP.get(dataId);
+ if (configListeners != null) {
+ configListeners.remove(entry);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ @Override
+ public Set getConfigListeners(String dataId) {
+ ConcurrentMap configListeners = CONFIG_LISTENERS_MAP.get(dataId);
+ if (CollectionUtils.isNotEmpty(configListeners)) {
+ return configListeners.keySet();
+ } else {
+ return null;
+ }
+ }
+
+ private static void notifyConfigMayChange(Map configMap) {
+ String configStr = ConfigStoreManager.convertConfig2Str(configMap);
+ CONFIG_LISTENER.onChangeEvent(new ConfigurationChangeEvent(CONFIG_NAMESPACE, configStr));
+ }
+
+
+ private static String getRaftUsernameKey() {
+ return String.join(ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR, ConfigurationKeys.FILE_ROOT_CONFIG, CONFIG_TYPE, USERNAME_KEY);
+ }
+ private static String getRaftPasswordKey() {
+ return String.join(ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR, ConfigurationKeys.FILE_ROOT_CONFIG, CONFIG_TYPE, PASSWORD_KEY);
+ }
+ private static String getRaftServerAddrKey() {
+ return String.join(ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR, ConfigurationKeys.FILE_ROOT_CONFIG, CONFIG_TYPE, SERVER_ADDR_KEY);
+ }
+
+ private static String getTokenExpireTimeInMillisecondsKey() {
+ return String.join(ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR, ConfigurationKeys.FILE_ROOT_CONFIG, CONFIG_TYPE, TOKEN_VALID_TIME_MS_KEY);
+ }
+
+ private static boolean isTokenExpired() {
+ if (tokenTimeStamp == -1) {
+ return true;
+ }
+ long tokenExpiredTime = tokenTimeStamp + TOKEN_EXPIRE_TIME_IN_MILLISECONDS;
+ return System.currentTimeMillis() >= tokenExpiredTime;
+ }
+
+ private static synchronized void refreshToken(String tcAddress) throws RetryableException {
+ // double-check if the token is expired inside the synchronized method to avoid repeated token refreshes in multiple threads
+ if (!isTokenExpired()) {
+ return;
+ }
+ // 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 param = new HashMap<>();
+ param.put(USERNAME_KEY, USERNAME);
+ param.put(PASSWORD_KEY, PASSWORD);
+
+ Map header = new HashMap<>();
+ header.put(HTTP.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
+ String response = null;
+ tokenTimeStamp = System.currentTimeMillis();
+ try (CloseableHttpResponse httpResponse =
+ HttpClientUtil.doPost(HTTP_PREFIX + tcAddress + "/api/v1/auth/login", param, header, 1000)) {
+ if (httpResponse != null) {
+ if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ response = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8);
+ 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.");
+ }
+ jwtToken = jsonNode.get("data").asText();
+ } else {
+ //authorized failed,throw exception to kill process
+ throw new AuthenticationFailedException("Authentication failed! you should configure the correct username and password.");
+ }
+ }
+ } catch (IOException e) {
+ throw new RetryableException(e.getMessage(), e);
+ }
+ }
+
+ private static class ConfigStoreListener implements ConfigurationChangeListener {
+ private final String dataId;
+ private final ConfigurationChangeListener listener;
+
+ public ConfigStoreListener(String dataId, ConfigurationChangeListener listener) {
+ this.dataId = dataId;
+ this.listener = listener;
+ }
+ @Override
+ public void onChangeEvent(ConfigurationChangeEvent event) {
+ if (CONFIG_NAMESPACE.equals(event.getDataId())) {
+ Properties seataConfigNew = new Properties();
+ Map newConfigMap = ConfigStoreManager.convertConfigStr2Map(event.getNewValue());
+ if (CollectionUtils.isNotEmpty(newConfigMap)) {
+ seataConfigNew.putAll(newConfigMap);
+ }
+ //Get all the monitored dataids and judge whether it has been modified
+ for (Map.Entry> entry : CONFIG_LISTENERS_MAP.entrySet()) {
+ String listenedDataId = entry.getKey();
+ String propertyOld = seataConfig.getProperty(listenedDataId, "");
+ String propertyNew = seataConfigNew.getProperty(listenedDataId, "");
+ if (!propertyOld.equals(propertyNew)) {
+ ConfigurationChangeEvent newEvent = new ConfigurationChangeEvent()
+ .setDataId(listenedDataId)
+ .setNewValue(propertyNew)
+ .setNamespace(CONFIG_NAMESPACE)
+ .setChangeType(ConfigurationChangeType.MODIFY);
+
+ // notify ConfigurationCache
+ ConcurrentMap configListeners = entry.getValue();
+ for (ConfigurationChangeListener configListener : configListeners.keySet()) {
+ configListener.onProcessEvent(newEvent);
+ }
+ }
+ }
+ seataConfig = seataConfigNew;
+ }
+ }
+ }
+
+}
diff --git a/config/seata-config-raft/src/main/java/org/apache/seata/config/raft/RaftConfigurationProvider.java b/config/seata-config-raft/src/main/java/org/apache/seata/config/raft/RaftConfigurationProvider.java
new file mode 100644
index 00000000000..c2470aa1bdd
--- /dev/null
+++ b/config/seata-config-raft/src/main/java/org/apache/seata/config/raft/RaftConfigurationProvider.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.config.raft;
+
+import org.apache.seata.common.loader.LoadLevel;
+import org.apache.seata.config.Configuration;
+import org.apache.seata.config.ConfigurationProvider;
+
+import static org.apache.seata.common.Constants.APPLICATION_TYPE_KEY;
+import static org.apache.seata.common.Constants.APPLICATION_TYPE_SERVER;
+
+@LoadLevel(name = "Raft", order = 1)
+public class RaftConfigurationProvider implements ConfigurationProvider {
+ @Override
+ public Configuration provide() {
+ // todo : optimize
+ String applicationType = System.getProperty(APPLICATION_TYPE_KEY);
+ if (APPLICATION_TYPE_SERVER.equals(applicationType)) {
+ return RaftConfigurationServer.getInstance();
+ } else {
+ return RaftConfigurationClient.getInstance();
+ }
+ }
+}
diff --git a/config/seata-config-raft/src/main/java/org/apache/seata/config/raft/RaftConfigurationServer.java b/config/seata-config-raft/src/main/java/org/apache/seata/config/raft/RaftConfigurationServer.java
new file mode 100644
index 00000000000..843e2b9f093
--- /dev/null
+++ b/config/seata-config-raft/src/main/java/org/apache/seata/config/raft/RaftConfigurationServer.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.config.raft;
+
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.seata.common.exception.NotSupportYetException;
+import org.apache.seata.common.util.CollectionUtils;
+import org.apache.seata.common.util.StringUtils;
+import org.apache.seata.config.AbstractConfiguration;
+import org.apache.seata.config.Configuration;
+import org.apache.seata.config.ConfigurationChangeEvent;
+import org.apache.seata.config.ConfigurationChangeListener;
+import org.apache.seata.config.ConfigurationChangeType;
+import org.apache.seata.config.ConfigurationFactory;
+import org.apache.seata.config.store.ConfigStoreManager;
+import org.apache.seata.config.store.ConfigStoreManagerFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.seata.common.ConfigurationKeys.CONFIG_STORE_DATA_ID;
+import static org.apache.seata.common.ConfigurationKeys.CONFIG_STORE_NAMESPACE;
+import static org.apache.seata.common.Constants.DEFAULT_STORE_DATA_ID;
+import static org.apache.seata.common.Constants.DEFAULT_STORE_NAMESPACE;
+
+
+public class RaftConfigurationServer extends AbstractConfiguration {
+ private static final Logger LOGGER = LoggerFactory.getLogger(RaftConfigurationServer.class);
+ private static volatile RaftConfigurationServer instance;
+ private static final Configuration FILE_CONFIG = ConfigurationFactory.CURRENT_FILE_INSTANCE;
+ private static ConfigStoreManager configStoreManager;
+ private static String CURRENT_NAMESPACE;
+ private static String CURRENT_DATA_ID;
+ private static final String CONFIG_TYPE = "raft";
+ private static volatile Properties seataConfig = new Properties();
+ private static final int MAP_INITIAL_CAPACITY = 8;
+ private static final ConcurrentMap> CONFIG_LISTENERS_MAP
+ = new ConcurrentHashMap<>(MAP_INITIAL_CAPACITY);
+
+ private static void initServerConfig() {
+ configStoreManager = ConfigStoreManagerFactory.getInstance();
+ CURRENT_NAMESPACE = FILE_CONFIG.getConfig(CONFIG_STORE_NAMESPACE, DEFAULT_STORE_NAMESPACE);
+ CURRENT_DATA_ID = FILE_CONFIG.getConfig(CONFIG_STORE_DATA_ID, DEFAULT_STORE_DATA_ID);
+ // load config from store
+ Map configMap = configStoreManager.getAll(CURRENT_NAMESPACE, CURRENT_DATA_ID);
+ seataConfig.putAll(configMap);
+ // build listener
+ ConfigStoreListener storeListener = new ConfigStoreListener(CURRENT_NAMESPACE, CURRENT_DATA_ID, null);
+ configStoreManager.addConfigListener(CURRENT_NAMESPACE, CURRENT_DATA_ID, storeListener);
+ }
+
+
+ public static RaftConfigurationServer getInstance() {
+ if (instance == null) {
+ synchronized (RaftConfigurationServer.class) {
+ if (instance == null) {
+ instance = new RaftConfigurationServer();
+ }
+ }
+ }
+ return instance;
+ }
+
+ private RaftConfigurationServer() {
+ initServerConfig();
+ }
+
+ @Override
+ public String getTypeName() {
+ return CONFIG_TYPE;
+ }
+
+ @Override
+ public boolean putConfig(String dataId, String content, long timeoutMills) {
+ throw new NotSupportYetException("not support operation putConfig");
+ }
+
+ @Override
+ public String getLatestConfig(String dataId, String defaultValue, long timeoutMills) {
+ String value = seataConfig.getProperty(dataId);
+ if (value == null) {
+ value = configStoreManager.get(CURRENT_NAMESPACE, CURRENT_DATA_ID, dataId);
+ }
+ return value == null ? defaultValue : value;
+ }
+
+ @Override
+ public boolean putConfigIfAbsent(String dataId, String content, long timeoutMills) {
+ throw new NotSupportYetException("not support atomic operation putConfigIfAbsent");
+ }
+
+ @Override
+ public boolean removeConfig(String dataId, long timeoutMills) {
+ throw new NotSupportYetException("not support operation removeConfig");
+ }
+
+ @Override
+ public void addConfigListener(String dataId, ConfigurationChangeListener listener) {
+ if (StringUtils.isBlank(dataId) || listener == null) {
+ return;
+ }
+ ConfigStoreListener storeListener = new ConfigStoreListener(CURRENT_NAMESPACE, dataId, listener);
+ CONFIG_LISTENERS_MAP.computeIfAbsent(dataId, key -> new ConcurrentHashMap<>())
+ .put(listener, storeListener);
+ configStoreManager.addConfigListener(CURRENT_NAMESPACE, dataId, storeListener);
+ }
+
+ @Override
+ public void removeConfigListener(String dataId, ConfigurationChangeListener listener) {
+ if (StringUtils.isBlank(dataId) || listener == null) {
+ return;
+ }
+ Set configChangeListeners = getConfigListeners(dataId);
+ if (CollectionUtils.isNotEmpty(configChangeListeners)) {
+ for (ConfigurationChangeListener entry : configChangeListeners) {
+ if (listener.equals(entry)) {
+ ConfigStoreListener storeListener = null;
+ Map configListeners = CONFIG_LISTENERS_MAP.get(dataId);
+ if (configListeners != null) {
+ storeListener = configListeners.get(listener);
+ configListeners.remove(entry);
+ }
+ if (storeListener != null) {
+ configStoreManager.removeConfigListener(CURRENT_NAMESPACE, dataId, storeListener);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ @Override
+ public Set getConfigListeners(String dataId) {
+ ConcurrentMap configListeners = CONFIG_LISTENERS_MAP.get(dataId);
+ if (CollectionUtils.isNotEmpty(configListeners)) {
+ return configListeners.keySet();
+ } else {
+ return null;
+ }
+ }
+
+
+ /**
+ * the type config change listener for raft config store
+ */
+ private static class ConfigStoreListener implements ConfigurationChangeListener {
+ private final String namespace;
+ private final String dataId;
+ private final ConfigurationChangeListener listener;
+
+ public ConfigStoreListener(String namespace, String dataId, ConfigurationChangeListener listener) {
+ this.namespace = namespace;
+ this.dataId = dataId;
+ this.listener = listener;
+ }
+
+ @Override
+ public void onChangeEvent(ConfigurationChangeEvent event) {
+ if (CURRENT_DATA_ID.equals(event.getDataId())) {
+ Properties seataConfigNew = new Properties();
+ seataConfigNew.putAll(configStoreManager.getAll(CURRENT_NAMESPACE, CURRENT_DATA_ID));
+
+ //Get all the monitored dataids and judge whether it has been modified
+ for (Map.Entry> entry : CONFIG_LISTENERS_MAP.entrySet()) {
+ String listenedDataId = entry.getKey();
+ String propertyOld = seataConfig.getProperty(listenedDataId, "");
+ String propertyNew = seataConfigNew.getProperty(listenedDataId, "");
+ if (!propertyOld.equals(propertyNew)) {
+ ConfigurationChangeEvent newEvent = new ConfigurationChangeEvent()
+ .setDataId(listenedDataId)
+ .setNewValue(propertyNew)
+ .setNamespace(CURRENT_NAMESPACE)
+ .setChangeType(ConfigurationChangeType.MODIFY);
+
+ ConcurrentMap configListeners = entry.getValue();
+ for (ConfigurationChangeListener configListener : configListeners.keySet()) {
+ configListener.onProcessEvent(newEvent);
+ }
+ }
+ }
+ seataConfig = seataConfigNew;
+ return;
+ }
+ // Compatible with old writing
+ listener.onProcessEvent(event);
+ }
+ }
+}
diff --git a/config/seata-config-raft/src/main/resources/META-INF/services/org.apache.seata.config.ConfigurationProvider b/config/seata-config-raft/src/main/resources/META-INF/services/org.apache.seata.config.ConfigurationProvider
new file mode 100644
index 00000000000..589333e6a30
--- /dev/null
+++ b/config/seata-config-raft/src/main/resources/META-INF/services/org.apache.seata.config.ConfigurationProvider
@@ -0,0 +1,17 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+org.apache.seata.config.raft.RaftConfigurationProvider
\ No newline at end of file
diff --git a/config/seata-config-raft/src/test/resources/META-INF/services/org.apache.seata.config.ConfigurationProvider b/config/seata-config-raft/src/test/resources/META-INF/services/org.apache.seata.config.ConfigurationProvider
new file mode 100644
index 00000000000..589333e6a30
--- /dev/null
+++ b/config/seata-config-raft/src/test/resources/META-INF/services/org.apache.seata.config.ConfigurationProvider
@@ -0,0 +1,17 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+org.apache.seata.config.raft.RaftConfigurationProvider
\ No newline at end of file
diff --git a/config/seata-config-raft/src/test/resources/registry.conf b/config/seata-config-raft/src/test/resources/registry.conf
new file mode 100644
index 00000000000..22313ebf407
--- /dev/null
+++ b/config/seata-config-raft/src/test/resources/registry.conf
@@ -0,0 +1,101 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+registry {
+ # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
+ type = "file"
+
+ nacos {
+ serverAddr = "localhost"
+ namespace = ""
+ cluster = "default"
+ }
+ eureka {
+ serviceUrl = "http://localhost:8761/eureka"
+ application = "default"
+ weight = "1"
+ }
+ redis {
+ serverAddr = "localhost:6379"
+ db = "0"
+ }
+ zk {
+ cluster = "default"
+ serverAddr = "127.0.0.1:2181"
+ sessionTimeout = 6000
+ connectTimeout = 2000
+ }
+ consul {
+ cluster = "default"
+ serverAddr = "127.0.0.1:8500"
+ }
+ etcd3 {
+ cluster = "default"
+ serverAddr = "http://localhost:2379"
+ }
+ sofa {
+ serverAddr = "127.0.0.1:9603"
+ application = "default"
+ region = "DEFAULT_ZONE"
+ datacenter = "DefaultDataCenter"
+ cluster = "default"
+ group = "SEATA_GROUP"
+ addressWaitTime = "3000"
+ }
+ file {
+ name = "file.conf"
+ }
+}
+
+config {
+ # file、nacos 、apollo、zk、consul、etcd3、raft
+ type = "file"
+
+ nacos {
+ serverAddr = "localhost"
+ namespace = ""
+ }
+ consul {
+ serverAddr = "127.0.0.1:8500"
+ }
+ apollo {
+ appId = "seata-server"
+ apolloMeta = "http://192.168.1.204:8801"
+ }
+ zk {
+ serverAddr = "127.0.0.1:2181"
+ sessionTimeout = 6000
+ connectTimeout = 2000
+ }
+ etcd3 {
+ serverAddr = "http://localhost:2379"
+ }
+ file {
+ name = "file.conf"
+ }
+ db {
+ type = "rocksdb"
+ dir = "configStore"
+ destroyOnShutdown = false
+ group = "SEATA_GROUP"
+ }
+ raft {
+ serverAddr = "127.0.0.1:7091"
+ username = "seata"
+ password = "seata"
+ }
+}
diff --git a/console/src/main/resources/static/console-fe/package-lock.json b/console/src/main/resources/static/console-fe/package-lock.json
index 45ca71e4923..e890819e093 100644
--- a/console/src/main/resources/static/console-fe/package-lock.json
+++ b/console/src/main/resources/static/console-fe/package-lock.json
@@ -13,6 +13,7 @@
"@alicloud/console-components-actions": "^1.1.1",
"@alicloud/console-components-app-layout": "^1.1.4",
"@alicloud/console-components-console-menu": "^1.2.12",
+ "@alifd/next": "^1.24.18",
"@babel/traverse": "^7.23.7",
"axios": "^1.7.4",
"browserify-sign": "^4.2.2",
diff --git a/console/src/main/resources/static/console-fe/package.json b/console/src/main/resources/static/console-fe/package.json
index 7acb85de0bd..568d556759a 100644
--- a/console/src/main/resources/static/console-fe/package.json
+++ b/console/src/main/resources/static/console-fe/package.json
@@ -78,6 +78,7 @@
"@alicloud/console-components-actions": "^1.1.1",
"@alicloud/console-components-app-layout": "^1.1.4",
"@alicloud/console-components-console-menu": "^1.2.12",
+ "@alifd/next": "^1.24.18",
"@babel/traverse": "^7.23.7",
"axios": "^1.7.4",
"browserify-sign": "^4.2.2",
diff --git a/console/src/main/resources/static/console-fe/src/app.tsx b/console/src/main/resources/static/console-fe/src/app.tsx
index d3bac4a24d0..b003efcbd52 100644
--- a/console/src/main/resources/static/console-fe/src/app.tsx
+++ b/console/src/main/resources/static/console-fe/src/app.tsx
@@ -78,7 +78,8 @@ class App extends React.Component {
get menu() {
const { locale }: AppPropsType = this.props;
const { MenuRouter = {} } = locale;
- const { overview, transactionInfo, globalLockInfo, sagaStatemachineDesigner } = MenuRouter;
+ const { overview, transactionInfo, globalLockInfo, configInfo, sagaStatemachineDesigner } = MenuRouter;
+
return {
items: [
// {
@@ -93,6 +94,10 @@ class App extends React.Component {
key: '/globallock/list',
label: globalLockInfo,
},
+ {
+ key: '/config/list',
+ label: configInfo,
+ },
{
key: '/sagastatemachinedesigner',
label: sagaStatemachineDesigner,
diff --git a/console/src/main/resources/static/console-fe/src/locales/en-us.ts b/console/src/main/resources/static/console-fe/src/locales/en-us.ts
index 449c61a3284..1e806741452 100644
--- a/console/src/main/resources/static/console-fe/src/locales/en-us.ts
+++ b/console/src/main/resources/static/console-fe/src/locales/en-us.ts
@@ -22,6 +22,7 @@ const enUs: ILocale = {
overview: 'Overview',
transactionInfo: 'TransactionInfo',
globalLockInfo: 'GlobalLockInfo',
+ configInfo: 'ConfigurationInfo',
sagaStatemachineDesigner: 'SagaStatemachineDesigner',
},
Header: {
@@ -88,6 +89,30 @@ const enUs: ILocale = {
operateTitle: 'operate',
deleteGlobalLockTitle: 'Delete global lock',
},
+ ConfigInfo: {
+ title: 'ConfigurationInfo',
+ subTitle: 'list',
+ resetButtonLabel: 'Reset',
+ searchButtonLabel: 'Search',
+ createButtonLabel: 'Create',
+ editButtonLabel: 'Edit',
+ deleteButtonLabel: 'Delete',
+ clearButtonLabel: 'Clear',
+ uploadButtonLabel: 'Upload',
+ operateTitle: 'Actions',
+ disableTitle: 'This page is only available if the Configuration Center type is raft mode',
+ editTitle: 'Edit Config',
+ deleteTitle: 'Delete Config',
+ deleteConfirmLabel: 'Are you sure you want to delete the configuration item with key: ',
+ deleteAllConfirmLabel: 'Are you sure you want to clear all configuration items in the following namespace and dataId: ',
+ addTitle: 'Add Config',
+ operationSuccess: 'Operation Success!',
+ operationFailed: 'Operation Failed',
+ inputFilterPlaceholder: 'Please select filter criteria',
+ fieldFillingTips: 'Please fill in the required fields',
+ uploadTitle: 'Upload Config',
+ uploadFileButtonLabel: 'Upload File',
+ },
};
export default enUs;
diff --git a/console/src/main/resources/static/console-fe/src/locales/zh-cn.ts b/console/src/main/resources/static/console-fe/src/locales/zh-cn.ts
index 089802559ae..8a4105ee16c 100644
--- a/console/src/main/resources/static/console-fe/src/locales/zh-cn.ts
+++ b/console/src/main/resources/static/console-fe/src/locales/zh-cn.ts
@@ -22,6 +22,7 @@ const zhCn: ILocale = {
overview: '概览',
transactionInfo: '事务信息',
globalLockInfo: '全局锁信息',
+ configInfo:'配置信息',
sagaStatemachineDesigner: 'Saga状态机设计器',
},
Header: {
@@ -88,6 +89,30 @@ const zhCn: ILocale = {
operateTitle: '操作',
deleteGlobalLockTitle: '删除全局锁',
},
+ ConfigInfo: {
+ title: '配置信息',
+ subTitle: '配置列表页',
+ resetButtonLabel: '重置',
+ searchButtonLabel: '搜索',
+ createButtonLabel: '创建',
+ editButtonLabel: '编辑',
+ deleteButtonLabel: '删除',
+ clearButtonLabel: '清空',
+ uploadButtonLabel: '上传',
+ operateTitle: '操作',
+ disableTitle: '该页面仅在配置中心类型为raft模式下可用',
+ editTitle: '编辑配置',
+ deleteTitle: '删除配置',
+ deleteConfirmLabel: '确认需要删除以下配置项: ',
+ deleteAllConfirmLabel: '确认需要清空以下namespace和dataId中的所有配置项: ',
+ addTitle: '新增配置',
+ operationSuccess: '操作成功!',
+ operationFailed: '操作失败',
+ inputFilterPlaceholder: '请选择筛选条件',
+ fieldFillingTips: '请将必要字段填充完整',
+ uploadTitle: '上传配置',
+ uploadFileButtonLabel: '上传文件',
+ },
};
export default zhCn;
diff --git a/console/src/main/resources/static/console-fe/src/pages/ConfigInfo/ConfigInfo.tsx b/console/src/main/resources/static/console-fe/src/pages/ConfigInfo/ConfigInfo.tsx
new file mode 100644
index 00000000000..47567171a5c
--- /dev/null
+++ b/console/src/main/resources/static/console-fe/src/pages/ConfigInfo/ConfigInfo.tsx
@@ -0,0 +1,644 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import {
+ ConfigProvider,
+ Table,
+ Button,
+ DatePicker,
+ Form,
+ Icon,
+ Pagination,
+ Input,
+ Dialog,
+ Message,
+ Select,
+ Upload,
+} from '@alicloud/console-components';
+import { withRouter } from 'react-router-dom';
+
+import {getConfig, getClusterInfo, putConfig, deleteConfig, deleteAllConfig, getAllNamespaces, getAllDataIds, uploadConfig} from "@/service/configInfo";
+import Page from '@/components/Page';
+import { GlobalProps } from '@/module';
+import styled, { css } from 'styled-components';
+import PropTypes from 'prop-types';
+import './index.scss';
+
+import moment from "moment/moment";
+type ConfigInfoState = {
+ configList: Array;
+ editDialogVisible: boolean;
+ deleteDialogVisible: boolean;
+ uploadDialogVisible: boolean;
+ loading: boolean;
+ configParam: ConfigParam;
+ editDialogInfo: DialogInfo;
+ deleteDialogInfo: DeleteDialogInfo;
+ uploadDialogInfo: UploadDialogInfo;
+ isRaft: boolean;
+ namespaces: Array;
+ dataIds: Array;
+}
+export type ConfigParam = {
+ namespace: string,
+ dataId: string,
+};
+
+type DialogInfo = {
+ isEdit: boolean;
+ namespace: string;
+ dataId: string;
+ key: string;
+ value: string;
+}
+
+type DeleteDialogInfo = {
+ namespace: string;
+ dataId: string;
+}
+
+type UploadDialogInfo = {
+ namespace: string;
+ dataId: string;
+ file: File;
+}
+
+const FormItem = Form.Item;
+
+
+
+class ConfigInfo extends React.Component {
+ static displayName = 'ConfigInfo';
+
+ static propTypes = {
+ locale: PropTypes.object,
+ history: PropTypes.object,
+ };
+
+ state: ConfigInfoState = {
+ configList: [],
+ loading: false,
+ editDialogVisible: false,
+ deleteDialogVisible: false,
+ uploadDialogVisible: false,
+ namespaces: [],
+ dataIds: [],
+ configParam: {
+ namespace: '',
+ dataId: '',
+ },
+ editDialogInfo: {
+ isEdit: false,
+ namespace: '',
+ dataId: '',
+ key: '',
+ value: '',
+ },
+ deleteDialogInfo: {
+ namespace: '',
+ dataId: '',
+ },
+ uploadDialogInfo: {
+ namespace: '',
+ dataId: '',
+ file: null,
+ },
+ isRaft: false,
+ }
+
+ componentDidMount() {
+ this.init();
+ this.pollingInterval = setInterval(this.refreshConfigData, 10000); // 每10秒刷新一次
+ }
+ componentWillUnmount() {
+ clearInterval(this.pollingInterval);
+ }
+
+ init = async () => {
+ const { disableTitle } = this.props.locale;
+ this.setState({ loading: true });
+ try {
+ const response = await getClusterInfo();
+ const raftMode = response.configMode
+ if (raftMode === 'raft') {
+ this.setState({ isRaft: true, loading: false });
+ this.fetchNamespaces();
+ } else {
+ this.setState({ loading: false });
+ Message.error(disableTitle);
+ setTimeout(() => this.props.history.goBack(), 1000);
+ }
+ //this.setState({ clusterInfo: result, loading: false });
+ } catch (error) {
+ Message.error('Failed to fetch cluster info');
+ this.setState({ loading: false });
+ this.props.history.goBack();
+ }
+ }
+
+ refreshConfigData = () => {
+ this.fetchNamespaces();
+ if (this.state.configParam.namespace) {
+ this.fetchDataIds(this.state.configParam.namespace);
+ }
+ }
+ fetchNamespaces = async () => {
+ try {
+ const response = await getAllNamespaces();
+ const result = response.result;
+ this.setState({ namespaces: result });
+ } catch (error) {
+ Message.error('Failed to fetch namespace list');
+ }
+ }
+
+ fetchDataIds = async (namespace: string) => {
+ try {
+ const response = await getAllDataIds({ namespace });
+ const result = response.result;
+ this.setState({ dataIds: result });
+ } catch (error) {
+ Message.error('Failed to fetch dataIds');
+ }
+ }
+
+ fetchConfigList = async () => {
+ this.setState({ loading: true });
+ try {
+ const response = await getConfig({namespace: this.state.configParam.namespace, dataId: this.state.configParam.dataId});
+ if (response.success && response.result){
+ const { config } = response.result;
+ console.log(config);
+ const configList = Object.keys(config).map((key) => ({ ...config[key] }));
+ this.setState({ configList, loading: false });
+ } else {
+ Message.error(response.errMsg || 'Failed to fetch config list');
+ this.setState({ loading: false });
+ }
+ } catch (error) {
+ Message.error('Failed to fetch config list');
+ this.setState({ loading: false });
+ }
+ }
+ searchFilterOnChange = async (key:string, val:string) => {
+ this.setState({
+ configParam: Object.assign(this.state.configParam,
+ { [key]: val }),
+ });
+ if (key === 'namespace') {
+ this.setState({
+ configParam: Object.assign(this.state.configParam,
+ { dataId: '' }),
+ });
+ await this.fetchDataIds(val);
+ }
+ }
+ search = () => {
+ this.fetchConfigList();
+ }
+ resetSearchFilter = () => {
+ this.setState({
+ configParam: {
+ // pagination info don`t reset
+ namespace: '',
+ dataId: '',
+ },
+ });
+ }
+
+ resetDialog = () => {
+ this.setState({
+ editDialogInfo: {isEdit: true, namespace: this.state.configParam.namespace, dataId: this.state.configParam.dataId, key: '', value: ''},
+ deleteDialogInfo: {namespace: '', dataId: ''},
+ uploadDialogInfo: {namespace: '', dataId: '', file: null},
+ });
+ };
+ openEditDialog = (config: { key: string; value: string }) => {
+ this.setState({ editDialogVisible: true, editDialogInfo: {isEdit: true, namespace: this.state.configParam.namespace, dataId: this.state.configParam.dataId, ...config}});
+ };
+
+ openDeleteDialog = () => {
+ this.setState({ deleteDialogVisible: true, deleteDialogInfo: {namespace: this.state.configParam.namespace, dataId: this.state.configParam.dataId}});
+ }
+
+ openUploadDialog = () => {
+ this.setState({ uploadDialogVisible: true, uploadDialogInfo: {namespace: this.state.configParam.namespace, dataId: '', file: null}});
+ }
+
+ createConfig = () => {
+ this.setState({ editDialogVisible: true, editDialogInfo: {isEdit: false, namespace: this.state.configParam.namespace, dataId: this.state.configParam.dataId, key: '', value: ''}});
+ };
+
+ closeDialog = () => {
+ this.setState({ editDialogVisible: false, deleteDialogVisible: false, uploadDialogVisible: false});
+ this.resetDialog();
+ };
+
+ handleAddOrEditConfig = async () => {
+ const { operationSuccess, operationFail } = this.props.locale;
+ const { editDialogInfo } = this.state;
+ try {
+ const response =await putConfig({
+ namespace: editDialogInfo.namespace,
+ dataId: editDialogInfo.dataId,
+ key: editDialogInfo.key,
+ value: editDialogInfo.value,
+ });
+ if (response.success) {
+ Message.success(operationSuccess);
+ this.setState({
+ editDialogVisible: false,
+ configParam: {
+ namespace: editDialogInfo.namespace,
+ dataId: editDialogInfo.dataId,
+ }
+ });
+ this.fetchNamespaces();
+ this.fetchDataIds(editDialogInfo.namespace);
+ this.fetchConfigList();
+ } else {
+ Message.error(response.errMsg || operationFail);
+ }
+ } catch (error) {
+ Message.error(operationFail);
+ }
+ }
+
+ handleDeleteConfig = async (record: { key: string }) => {
+ const { deleteTitle, deleteConfirmLabel, operationSuccess, operationFail } = this.props.locale;
+ Dialog.confirm({
+ title: deleteTitle,
+ content: deleteConfirmLabel + `${record.key} ?`,
+ onOk: async () => {
+ try {
+ const response = await deleteConfig({ namespace: this.state.configParam.namespace, dataId: this.state.configParam.dataId, key: record.key });
+ if (response.success) {
+ Message.success(operationSuccess);
+ this.fetchConfigList();
+ } else {
+ Message.error(response.errMsg || operationFail);
+ }
+ } catch (error) {
+ Message.error(operationFail);
+ }
+ },
+ onCancel: () => {
+
+ },
+ });
+ }
+
+ handleDeleteAllConfig = async () => {
+ const {operationSuccess, operationFail, fieldFillingTips} = this.props.locale;
+ const { namespace, dataId } = this.state.deleteDialogInfo;
+
+ if (!namespace || !dataId) {
+ Message.error(fieldFillingTips);
+ return;
+ }
+ try {
+ const response = await deleteAllConfig({ namespace, dataId });
+ if (response.success) {
+ Message.success(operationSuccess);
+ this.setState({
+ configParam: {
+ namespace: namespace,
+ dataId: dataId,
+ },
+ deleteDialogVisible: false,
+ deleteDialogInfo: { namespace: '', dataId: '' },
+ });
+ this.fetchDataIds(namespace)
+ this.fetchConfigList();
+ } else {
+ Message.error(response.errMsg || operationFail);
+ }
+ } catch (error) {
+ Message.error(operationFail);
+ }
+ }
+
+ handleUploadConfig = async () => {
+ const {operationSuccess, operationFail, fieldFillingTips} = this.props.locale;
+ const { namespace, dataId,file } = this.state.uploadDialogInfo;
+ if (!namespace || !dataId || !file) {
+ Message.error(fieldFillingTips);
+ return;
+ }
+ const formData = new FormData();
+ formData.append('namespace', namespace);
+ formData.append('dataId', dataId);
+ formData.append('file', file);
+
+ try {
+ const response = await uploadConfig(formData);
+ if (response.success) {
+ Message.success(operationSuccess);
+ this.setState({
+ uploadDialogVisible: false,
+ uploadDialogInfo: { namespace: '', dataId: '', file: null},
+ configParam: {
+ namespace: namespace,
+ dataId: dataId,
+ }
+ });
+ this.fetchNamespaces();
+ this.fetchDataIds(namespace)
+ this.fetchConfigList();
+ } else {
+ Message.error(response.errMsg || operationFail);
+ }
+ } catch (error) {
+ Message.error(operationFail);
+ }
+ }
+
+ handleDialogInputChange = (key: string, value: string) => {
+ this.setState((prevState) => ({
+ editDialogInfo: {
+ ...prevState.editDialogInfo,
+ [key]: value,
+ },
+ }));
+ };
+
+ handleDeleteDialogInputChange = (key: string, value: string) => {
+ this.setState((prevState) => ({
+ deleteDialogInfo: {
+ ...prevState.deleteDialogInfo,
+ [key]: value
+ }
+ }))
+ }
+
+ handleUploadDialogInputChange = (key: string, value: string) => {
+ this.setState((prevState) => ({
+ uploadDialogInfo: {
+ ...prevState.uploadDialogInfo,
+ [key]: value
+ }
+ }))
+ }
+
+ handleFileInputChange = (fileList: Array) => {
+ const file = fileList.length > 0 ? fileList[0] : null;
+ if (file && file.originFileObj) {
+ this.handleUploadDialogInputChange('file', file.originFileObj);
+ }
+ }
+ render() {
+ const { locale = {} } = this.props;
+ const { title, subTitle,
+ searchButtonLabel,
+ resetButtonLabel,
+ createButtonLabel,
+ clearButtonLabel,
+ uploadButtonLabel,
+ operateTitle,
+ editTitle,
+ deleteTitle,
+ uploadTitle,
+ deleteAllConfirmLabel,
+ editButtonLabel,
+ deleteButtonLabel,
+ inputFilterPlaceholder,
+ uploadFileButtonLabel,
+ } = locale;
+
+ return (
+
+ {/* search form */}
+
+
+ {/* config info table */}
+
+
+
+
+
+
+ (
+ <>
+
+
+ >
+ )}
+ />
+
+
+ {/* config edit dialog */}
+
+
+ {/* config delete dialog*/}
+
+
+ {/* config upload dialog*/}
+
+
+ );
+ }
+}
+export default withRouter(ConfigProvider.config(ConfigInfo, {}));
diff --git a/console/src/main/resources/static/console-fe/src/pages/ConfigInfo/index.scss b/console/src/main/resources/static/console-fe/src/pages/ConfigInfo/index.scss
new file mode 100644
index 00000000000..2944f981947
--- /dev/null
+++ b/console/src/main/resources/static/console-fe/src/pages/ConfigInfo/index.scss
@@ -0,0 +1,16 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
diff --git a/console/src/main/resources/static/console-fe/src/pages/ConfigInfo/index.ts b/console/src/main/resources/static/console-fe/src/pages/ConfigInfo/index.ts
new file mode 100644
index 00000000000..d30c8ca5a93
--- /dev/null
+++ b/console/src/main/resources/static/console-fe/src/pages/ConfigInfo/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import ConfigInfo from './ConfigInfo';
+
+export * from './ConfigInfo';
+
+export default ConfigInfo;
diff --git a/console/src/main/resources/static/console-fe/src/router.tsx b/console/src/main/resources/static/console-fe/src/router.tsx
index d881b472d02..88c7be6b920 100644
--- a/console/src/main/resources/static/console-fe/src/router.tsx
+++ b/console/src/main/resources/static/console-fe/src/router.tsx
@@ -18,10 +18,12 @@ import { HashRouter, Route, Switch, Redirect } from 'react-router-dom';
import Overview from '@/pages/Overview';
import TransactionInfo from '@/pages/TransactionInfo';
import GlobalLockInfo from './pages/GlobalLockInfo';
+import ConfigInfo from "./pages/ConfigInfo";
export default [
// { path: '/', exact: true, render: () => },
// { path: '/Overview', component: Overview },
{ path: '/transaction/list', component: TransactionInfo },
{ path: '/globallock/list', component: GlobalLockInfo },
+ { path: '/config/list', component: ConfigInfo },
];
diff --git a/console/src/main/resources/static/console-fe/src/service/configInfo.ts b/console/src/main/resources/static/console-fe/src/service/configInfo.ts
new file mode 100644
index 00000000000..37e4658d689
--- /dev/null
+++ b/console/src/main/resources/static/console-fe/src/service/configInfo.ts
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {configRequest} from '@/utils/request';
+
+
+export async function getConfig(params: { namespace: string, dataId: string}): Promise {
+ const result = await configRequest('/config/getAll', {
+ method: 'get',
+ params,
+ });
+ return result;
+}
+
+export async function putConfig(params: { namespace: string, dataId: string, key: string, value: string}): Promise {
+ const result = await configRequest('/config/put', {
+ method: 'post',
+ params,
+ });
+ return result;
+}
+
+export async function deleteConfig(params: { namespace: string, dataId: string, key: string }): Promise {
+ const result = await configRequest('/config/delete', {
+ method: 'delete',
+ params,
+ });
+ return result;
+}
+
+export async function deleteAllConfig(params: { namespace: string, dataId: string}): Promise {
+ const result = await configRequest('/config/deleteAll', {
+ method: 'delete',
+ params,
+ });
+ return result;
+}
+
+export async function uploadConfig(formData: FormData): Promise {
+ const result = await configRequest('/config/upload', {
+ method: 'post',
+ data: formData,
+ });
+ return result;
+}
+
+export async function getClusterInfo(): Promise {
+ const result = await configRequest('/config/cluster', {
+ method: 'get',
+ });
+ return result;
+}
+
+export async function getAllNamespaces(): Promise {
+ const result = await configRequest('/config/getNamespaces', {
+ method: 'get',
+ });
+ return result;
+}
+
+export async function getAllDataIds(params: { namespace: string}): Promise {
+ const result = await configRequest('/config/getDataIds', {
+ method: 'get',
+ params,
+ });
+ return result;
+}
diff --git a/console/src/main/resources/static/console-fe/src/utils/request.ts b/console/src/main/resources/static/console-fe/src/utils/request.ts
index 47ab7597615..e28c6a2e5c6 100644
--- a/console/src/main/resources/static/console-fe/src/utils/request.ts
+++ b/console/src/main/resources/static/console-fe/src/utils/request.ts
@@ -90,3 +90,50 @@ const request = () => {
};
export default request();
+
+
+const clusterRequest = () => {
+ const instance: AxiosInstance = axios.create({
+ baseURL: 'metadata/v1',
+ method: 'get',
+ });
+
+ instance.interceptors.request.use((config: AxiosRequestConfig) => {
+ let authHeader: string | null = localStorage.getItem(AUTHORIZATION_HEADER);
+ // add jwt header
+ config.headers[AUTHORIZATION_HEADER] = authHeader;
+ return config;
+ })
+
+ instance.interceptors.response.use(
+ (response: AxiosResponse): Promise => {
+ const isSuccess = get(response, 'data.success');
+ if (response.status === 200 || isSuccess) {
+ return Promise.resolve(get(response, 'data'));
+ } else {
+ const errorText =
+ get(response, 'data.errMsg') ||
+ response.statusText;
+ Message.error(errorText);
+ return Promise.reject(response);
+ }
+ },
+ error => {
+ if (error.response) {
+ const { status } = error.response;
+ if (status === 403 || status === 401) {
+ (window as any).globalHistory.replace('/login');
+ return;
+ }
+ Message.error(`HTTP ERROR: ${status}`);
+ } else {
+ Message.error(API_GENERAL_ERROR_MESSAGE);
+ }
+ return Promise.reject(error);
+ }
+ );
+
+ return instance;
+};
+
+export const configRequest = clusterRequest();
diff --git a/dependencies/pom.xml b/dependencies/pom.xml
index fb787080a70..b730c56f224 100644
--- a/dependencies/pom.xml
+++ b/dependencies/pom.xml
@@ -78,6 +78,8 @@
4.1.101.Final
4.0.3
1.6.7
+ 8.8.1
+
3.25.4
1.55.1
5.4.0
@@ -239,6 +241,11 @@
bolt
${sofa.bolt.version}
+
+ org.rocksdb
+ rocksdbjni
+ ${rocksdbjni.version}
+
com.alibaba
fastjson
@@ -907,4 +914,4 @@
-
+
\ No newline at end of file
diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/SeataCoreEnvironmentPostProcessor.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/SeataCoreEnvironmentPostProcessor.java
index bfa4923469f..4038c55ca4f 100644
--- a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/SeataCoreEnvironmentPostProcessor.java
+++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/SeataCoreEnvironmentPostProcessor.java
@@ -17,6 +17,7 @@
package org.apache.seata.spring.boot.autoconfigure;
import java.util.concurrent.atomic.AtomicBoolean;
+
import org.apache.seata.spring.boot.autoconfigure.properties.LogProperties;
import org.apache.seata.spring.boot.autoconfigure.properties.ShutdownProperties;
import org.apache.seata.spring.boot.autoconfigure.properties.ThreadFactoryProperties;
@@ -28,6 +29,8 @@
import org.apache.seata.spring.boot.autoconfigure.properties.config.ConfigFileProperties;
import org.apache.seata.spring.boot.autoconfigure.properties.config.ConfigNacosProperties;
import org.apache.seata.spring.boot.autoconfigure.properties.config.ConfigProperties;
+import org.apache.seata.spring.boot.autoconfigure.properties.config.ConfigRaftProperties;
+import org.apache.seata.spring.boot.autoconfigure.properties.config.ConfigStoreProperties;
import org.apache.seata.spring.boot.autoconfigure.properties.config.ConfigZooKeeperProperties;
import org.apache.seata.spring.boot.autoconfigure.properties.registry.RegistryConsulProperties;
import org.apache.seata.spring.boot.autoconfigure.properties.registry.RegistryCustomProperties;
@@ -46,6 +49,7 @@
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
+
import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.CONFIG_APOLLO_PREFIX;
import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.CONFIG_CONSUL_PREFIX;
import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.CONFIG_CUSTOM_PREFIX;
@@ -53,6 +57,8 @@
import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.CONFIG_FILE_PREFIX;
import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.CONFIG_NACOS_PREFIX;
import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.CONFIG_PREFIX;
+import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.CONFIG_RAFT_PREFIX;
+import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.CONFIG_STORE_PREFIX;
import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.CONFIG_ZK_PREFIX;
import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.LOG_PREFIX;
import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.PROPERTY_BEAN_MAP;
@@ -72,7 +78,6 @@
import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.TRANSPORT_PREFIX;
import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.REGISTRY_METADATA_PREFIX;
-
public class SeataCoreEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
private static final AtomicBoolean INIT = new AtomicBoolean(false);
@@ -100,6 +105,8 @@ public static void init() {
PROPERTY_BEAN_MAP.put(CONFIG_APOLLO_PREFIX, ConfigApolloProperties.class);
PROPERTY_BEAN_MAP.put(CONFIG_ETCD3_PREFIX, ConfigEtcd3Properties.class);
PROPERTY_BEAN_MAP.put(CONFIG_CUSTOM_PREFIX, ConfigCustomProperties.class);
+ PROPERTY_BEAN_MAP.put(CONFIG_STORE_PREFIX, ConfigStoreProperties.class);
+ PROPERTY_BEAN_MAP.put(CONFIG_RAFT_PREFIX, ConfigRaftProperties.class);
PROPERTY_BEAN_MAP.put(REGISTRY_CONSUL_PREFIX, RegistryConsulProperties.class);
PROPERTY_BEAN_MAP.put(REGISTRY_ETCD3_PREFIX, RegistryEtcd3Properties.class);
diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/StarterConstants.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/StarterConstants.java
index 074b46c6a57..a6c2b676ef3 100644
--- a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/StarterConstants.java
+++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/StarterConstants.java
@@ -68,8 +68,8 @@ public interface StarterConstants {
String CONFIG_ZK_PREFIX = CONFIG_PREFIX + ".zk";
String CONFIG_FILE_PREFIX = CONFIG_PREFIX + ".file";
String CONFIG_CUSTOM_PREFIX = CONFIG_PREFIX + ".custom";
-
-
+ String CONFIG_RAFT_PREFIX = CONFIG_PREFIX + ".raft";
+ String CONFIG_STORE_PREFIX = CONFIG_RAFT_PREFIX + ".db";
String SERVER_PREFIX = SEATA_PREFIX + ".server";
String SERVER_RATELIMIT_PREFIX = SERVER_PREFIX + ".ratelimit";
String SERVER_UNDO_PREFIX = SERVER_PREFIX + ".undo";
diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/loader/SeataPropertiesLoader.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/loader/SeataPropertiesLoader.java
index 4496c3dc98b..bb1b195ea6f 100644
--- a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/loader/SeataPropertiesLoader.java
+++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/loader/SeataPropertiesLoader.java
@@ -48,7 +48,7 @@
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SeataPropertiesLoader implements ApplicationContextInitializer {
-
+
List prefixList = Arrays.asList(FILE_ROOT_PREFIX_CONFIG, FILE_ROOT_PREFIX_REGISTRY, SERVER_PREFIX,
STORE_PREFIX, METRICS_PREFIX, TRANSPORT_PREFIX);
diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/config/ConfigRaftProperties.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/config/ConfigRaftProperties.java
new file mode 100644
index 00000000000..af2fff40e9b
--- /dev/null
+++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/config/ConfigRaftProperties.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.spring.boot.autoconfigure.properties.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.CONFIG_RAFT_PREFIX;
+
+@Component
+@ConfigurationProperties(prefix = CONFIG_RAFT_PREFIX)
+public class ConfigRaftProperties {
+ private String serverAddr;
+
+ private Long metadataMaxAgeMs = 30000L;
+
+ private String username;
+
+ private String password;
+
+ private Long tokenValidityInMilliseconds = 29 * 60 * 1000L;
+
+ public Long getMetadataMaxAgeMs() {
+ return metadataMaxAgeMs;
+ }
+
+ public ConfigRaftProperties setMetadataMaxAgeMs(Long metadataMaxAgeMs) {
+ this.metadataMaxAgeMs = metadataMaxAgeMs;
+ return this;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public ConfigRaftProperties setUsername(String username) {
+ this.username = username;
+ return this;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public ConfigRaftProperties setPassword(String password) {
+ this.password = password;
+ return this;
+ }
+
+ public Long getTokenValidityInMilliseconds() {
+ return tokenValidityInMilliseconds;
+ }
+
+ public ConfigRaftProperties setTokenValidityInMilliseconds(Long tokenValidityInMilliseconds) {
+ this.tokenValidityInMilliseconds = tokenValidityInMilliseconds;
+ return this;
+ }
+
+ public String getServerAddr() {
+ return serverAddr;
+ }
+
+ public ConfigRaftProperties setServerAddr(String serverAddr) {
+ this.serverAddr = serverAddr;
+ return this;
+ }
+
+}
diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/config/ConfigStoreProperties.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/config/ConfigStoreProperties.java
new file mode 100644
index 00000000000..3e361ba1f3f
--- /dev/null
+++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/config/ConfigStoreProperties.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.spring.boot.autoconfigure.properties.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.CONFIG_STORE_PREFIX;
+
+
+@Component
+@ConfigurationProperties(prefix = CONFIG_STORE_PREFIX)
+public class ConfigStoreProperties {
+ /**
+ * rocksdb, (leveldb, caffeine)
+ */
+ private String type = "rocksdb";
+ private String dir = "configStore";
+ private boolean destroyOnShutdown = false;
+ private String namespace = "default";
+ private String dataId = "seata.properties";
+
+ public String getType() {
+ return type;
+ }
+
+ public ConfigStoreProperties setType(String type) {
+ this.type = type;
+ return this;
+ }
+
+ public String getDir() {
+ return dir;
+ }
+
+ public ConfigStoreProperties setDir(String dir) {
+ this.dir = dir;
+ return this;
+ }
+
+ public boolean isDestroyOnShutdown() {
+ return destroyOnShutdown;
+ }
+
+ public ConfigStoreProperties setDestroyOnShutdown(boolean destroyOnShutdown) {
+ this.destroyOnShutdown = destroyOnShutdown;
+ return this;
+ }
+
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public ConfigStoreProperties setNamespace(String namespace) {
+ this.namespace = namespace;
+ return this;
+ }
+
+ public String getDataId() {
+ return dataId;
+ }
+
+ public ConfigStoreProperties setDataId(String dataId) {
+ this.dataId = dataId;
+ return this;
+ }
+}
diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/config/ConfigRaftPropertiesTest.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/config/ConfigRaftPropertiesTest.java
new file mode 100644
index 00000000000..62afe4d6b37
--- /dev/null
+++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/config/ConfigRaftPropertiesTest.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.spring.boot.autoconfigure.properties.config;
+
+import org.apache.seata.common.loader.EnhancedServiceLoader;
+import org.apache.seata.config.Configuration;
+import org.apache.seata.config.ExtConfigurationProvider;
+import org.apache.seata.config.FileConfiguration;
+import org.apache.seata.spring.boot.autoconfigure.BasePropertiesTest;
+import org.apache.seata.spring.boot.autoconfigure.provider.SpringApplicationContextProvider;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+
+
+@org.springframework.context.annotation.Configuration
+@Import(SpringApplicationContextProvider.class)
+public class ConfigRaftPropertiesTest extends BasePropertiesTest {
+ @Bean("testConfigRaftProperties")
+ public ConfigRaftProperties configRaftProperties() {
+ return new ConfigRaftProperties().setUsername(STR_TEST_AAA).setPassword(STR_TEST_BBB).setServerAddr(STR_TEST_CCC).setMetadataMaxAgeMs((long)LONG_TEST_ONE).setTokenValidityInMilliseconds((long)LONG_TEST_TWO);
+ }
+
+ @Test
+ public void testConfigRaftProperties() {
+ FileConfiguration configuration = mock(FileConfiguration.class);
+ Configuration currentConfiguration = EnhancedServiceLoader.load(ExtConfigurationProvider.class).provide(configuration);
+
+ assertEquals(STR_TEST_AAA, currentConfiguration.getConfig("config.raft.username"));
+ assertEquals(STR_TEST_BBB, currentConfiguration.getConfig("config.raft.password"));
+ assertEquals(STR_TEST_CCC, currentConfiguration.getConfig("config.raft.serverAddr"));
+ assertEquals(LONG_TEST_ONE, currentConfiguration.getInt("config.raft.metadataMaxAgeMs"));
+ assertEquals(LONG_TEST_TWO, currentConfiguration.getInt("config.raft.tokenValidityInMilliseconds"));
+
+ }
+}
diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/config/ConfigStorePropertiesTest.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/config/ConfigStorePropertiesTest.java
new file mode 100644
index 00000000000..e1f16469d47
--- /dev/null
+++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/config/ConfigStorePropertiesTest.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.spring.boot.autoconfigure.properties.config;
+
+import org.apache.seata.common.loader.EnhancedServiceLoader;
+import org.apache.seata.config.Configuration;
+import org.apache.seata.config.ExtConfigurationProvider;
+import org.apache.seata.config.FileConfiguration;
+import org.apache.seata.spring.boot.autoconfigure.BasePropertiesTest;
+import org.apache.seata.spring.boot.autoconfigure.provider.SpringApplicationContextProvider;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.mockito.Mockito.mock;
+
+@org.springframework.context.annotation.Configuration
+@Import(SpringApplicationContextProvider.class)
+public class ConfigStorePropertiesTest extends BasePropertiesTest {
+ @Bean("testConfigStoreProperties")
+ public ConfigStoreProperties configStoreProperties() {
+ return new ConfigStoreProperties().setType(STR_TEST_AAA).setDir(STR_TEST_BBB).setDestroyOnShutdown(false).setNamespace(STR_TEST_DDD).setDataId(STR_TEST_EEE);
+ }
+
+ @Test
+ public void testConfigStoreProperties() {
+ FileConfiguration configuration = mock(FileConfiguration.class);
+ Configuration currentConfiguration = EnhancedServiceLoader.load(ExtConfigurationProvider.class).provide(configuration);
+
+ assertEquals(STR_TEST_AAA, currentConfiguration.getConfig("config.raft.db.type"));
+ assertEquals(STR_TEST_BBB, currentConfiguration.getConfig("config.raft.db.dir"));
+ assertFalse(currentConfiguration.getBoolean("config.raft.db.destroyOnShutdown"));
+ assertEquals(STR_TEST_DDD, currentConfiguration.getConfig("config.raft.db.namespace"));
+ assertEquals(STR_TEST_EEE, currentConfiguration.getConfig("config.raft.db.dataId"));
+ }
+}
diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/resources/application-test.properties b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/resources/application-test.properties
index c492f36092b..97d6ffb9d3f 100755
--- a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/resources/application-test.properties
+++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/resources/application-test.properties
@@ -51,3 +51,15 @@ seata.config.zk.password=ddd
seata.config.zk.node-path=aaa
seata.config.custom.name=aaa
+
+seata.config.raft.db.type=aaa
+seata.config.raft.db.dir=bbb
+seata.config.raft.db.destroy-on-shutdown=false
+seata.config.raft.db.namespace=ddd
+seata.config.raft.db.data-id=eee
+
+seata.config.raft.username=aaa
+seata.config.raft.password=bbb
+seata.config.raft.server-addr=ccc
+seata.config.raft.metadata-max-age-ms=1
+seata.config.raft.token-validity-in-milliseconds=2
diff --git a/server/src/main/java/org/apache/seata/server/Server.java b/server/src/main/java/org/apache/seata/server/Server.java
index b93ec5fb080..175d1a13e1b 100644
--- a/server/src/main/java/org/apache/seata/server/Server.java
+++ b/server/src/main/java/org/apache/seata/server/Server.java
@@ -105,6 +105,8 @@ public void start(String[] args) {
nettyRemotingServer.setHandler(coordinator);
Optional.ofNullable(seataInstanceStrategy).ifPresent(SeataInstanceStrategy::init);
// let ServerRunner do destroy instead ShutdownHook, see https://github.com/seata/seata/issues/4028
+
+ ServerRunner.addToFirstDisposable(coordinator);
ServerRunner.addDisposable(coordinator);
nettyRemotingServer.init();
}
diff --git a/server/src/main/java/org/apache/seata/server/ServerApplication.java b/server/src/main/java/org/apache/seata/server/ServerApplication.java
index 952187137e3..9c5e1ccf86b 100644
--- a/server/src/main/java/org/apache/seata/server/ServerApplication.java
+++ b/server/src/main/java/org/apache/seata/server/ServerApplication.java
@@ -21,11 +21,15 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import static org.apache.seata.common.Constants.APPLICATION_TYPE_KEY;
+import static org.apache.seata.common.Constants.APPLICATION_TYPE_SERVER;
+
/**
*/
@SpringBootApplication(scanBasePackages = {"org.apache.seata"})
public class ServerApplication {
public static void main(String[] args) throws IOException {
+ System.setProperty(APPLICATION_TYPE_KEY, APPLICATION_TYPE_SERVER);
// run the spring-boot application
SpringApplication.run(ServerApplication.class, args);
}
diff --git a/server/src/main/java/org/apache/seata/server/ServerRunner.java b/server/src/main/java/org/apache/seata/server/ServerRunner.java
index a48c7379fdf..ba0fe8066dd 100644
--- a/server/src/main/java/org/apache/seata/server/ServerRunner.java
+++ b/server/src/main/java/org/apache/seata/server/ServerRunner.java
@@ -54,9 +54,14 @@ public static void addDisposable(Disposable disposable) {
DISPOSABLE_LIST.add(disposable);
}
+ public static void addToFirstDisposable(Disposable disposable) {
+ DISPOSABLE_LIST.add(0, disposable);
+ }
+
@Resource
Server seataServer;
+
@Override
public void run(String... args) {
try {
@@ -83,7 +88,7 @@ public boolean started() {
public void destroy() throws Exception {
if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("destoryAll starting");
+ LOGGER.debug("destory All starting");
}
for (Disposable disposable : DISPOSABLE_LIST) {
@@ -91,7 +96,7 @@ public void destroy() throws Exception {
}
if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("destoryAll finish");
+ LOGGER.debug("destory All finish");
}
}
diff --git a/server/src/main/java/org/apache/seata/server/cluster/listener/ClusterConfigChangeEvent.java b/server/src/main/java/org/apache/seata/server/cluster/listener/ClusterConfigChangeEvent.java
new file mode 100644
index 00000000000..fd9461bcdbd
--- /dev/null
+++ b/server/src/main/java/org/apache/seata/server/cluster/listener/ClusterConfigChangeEvent.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.server.cluster.listener;
+
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * The type ClusterConfigChangeEvent
+ */
+public class ClusterConfigChangeEvent extends ApplicationEvent {
+
+ private String namespace;
+ private String dataId;
+
+ public ClusterConfigChangeEvent(Object source, String namespace, String dataId) {
+ super(source);
+ this.namespace = namespace;
+ this.dataId = dataId;
+ }
+
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public void setNamespace(String namespace) {
+ this.namespace = namespace;
+ }
+
+ public String getDataId() {
+ return dataId;
+ }
+
+ public void setDataId(String dataId) {
+ this.dataId = dataId;
+ }
+}
diff --git a/server/src/main/java/org/apache/seata/server/cluster/listener/ClusterConfigChangeListener.java b/server/src/main/java/org/apache/seata/server/cluster/listener/ClusterConfigChangeListener.java
new file mode 100644
index 00000000000..4236a15bc71
--- /dev/null
+++ b/server/src/main/java/org/apache/seata/server/cluster/listener/ClusterConfigChangeListener.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.server.cluster.listener;
+
+public interface ClusterConfigChangeListener {
+
+ /**
+ * cluster config change event
+ * @param event event
+ */
+ void onChangeEvent(ClusterConfigChangeEvent event);
+}
diff --git a/server/src/main/java/org/apache/seata/server/cluster/manager/ClusterConfigWatcherManager.java b/server/src/main/java/org/apache/seata/server/cluster/manager/ClusterConfigWatcherManager.java
new file mode 100644
index 00000000000..4e81fbff1e5
--- /dev/null
+++ b/server/src/main/java/org/apache/seata/server/cluster/manager/ClusterConfigWatcherManager.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.server.cluster.manager;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.AsyncContext;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.seata.common.thread.NamedThreadFactory;
+import org.apache.seata.common.util.CollectionUtils;
+import org.apache.seata.server.cluster.listener.ClusterConfigChangeEvent;
+import org.apache.seata.server.cluster.listener.ClusterConfigChangeListener;
+import org.apache.seata.server.cluster.watch.ConfigWatcher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+/**
+ *
+ * The type of cluster config watcher manager.
+ */
+@Component
+public class ClusterConfigWatcherManager implements ClusterConfigChangeListener {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ClusterConfigWatcherManager.class);
+
+ private static final Map>>> WATCHERS = new ConcurrentHashMap<>();
+
+ private final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor =
+ new ScheduledThreadPoolExecutor(1, new NamedThreadFactory("long-polling", 1));
+
+ @PostConstruct
+ public void init() {
+ // Responds to monitors that time out
+ scheduledThreadPoolExecutor.scheduleWithFixedDelay(() -> {
+ for (String namespace : WATCHERS.keySet()) {
+ Map>> dataIdWatchersMap = WATCHERS.get(namespace);
+ for (String dataId : dataIdWatchersMap.keySet()) {
+ Optional.ofNullable(dataIdWatchersMap.remove(dataId))
+ .ifPresent(watchers -> watchers.parallelStream().forEach(watcher -> {
+ if (System.currentTimeMillis() >= watcher.getTimeout()) {
+ HttpServletResponse httpServletResponse =
+ (HttpServletResponse)((AsyncContext)watcher.getAsyncContext()).getResponse();
+ watcher.setDone(true);
+ httpServletResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+ ((AsyncContext)watcher.getAsyncContext()).complete();
+ }
+ if (!watcher.isDone()) {
+ // Re-register
+ registryWatcher(watcher);
+ }
+ }));
+ }
+ }
+ }, 1, 1, TimeUnit.SECONDS);
+ }
+ @Override
+ @EventListener
+ @Async
+ public void onChangeEvent(ClusterConfigChangeEvent event) {
+ String namespace = event.getNamespace();
+ String dataId = event.getDataId();
+ Map>> dataIdWatchersMap = WATCHERS.get(namespace);
+ if (CollectionUtils.isNotEmpty(dataIdWatchersMap)) {
+ Optional.ofNullable(dataIdWatchersMap.remove(dataId))
+ .ifPresent(watchers -> watchers.parallelStream().forEach(this::notify));
+ }
+ }
+
+ private void notify(ConfigWatcher> watcher) {
+ AsyncContext asyncContext = (AsyncContext)watcher.getAsyncContext();
+ HttpServletResponse httpServletResponse = (HttpServletResponse)asyncContext.getResponse();
+ watcher.setDone(true);
+ LOGGER.info("notify cluster config change event to: {}", asyncContext.getRequest().getRemoteAddr());
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("notify cluster config change event to: {}", asyncContext.getRequest().getRemoteAddr());
+ }
+ httpServletResponse.setStatus(HttpServletResponse.SC_OK);
+ asyncContext.complete();
+ }
+
+ public void registryWatcher(ConfigWatcher> watcher) {
+ String namespace = watcher.getNamespace();
+ String dataId = watcher.getDataId();
+ WATCHERS.computeIfAbsent(namespace, ns -> new ConcurrentHashMap<>())
+ .computeIfAbsent(dataId, did -> new ConcurrentLinkedQueue<>()).add(watcher);
+ }
+}
diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/RaftConfigInitializer.java b/server/src/main/java/org/apache/seata/server/cluster/raft/RaftConfigInitializer.java
new file mode 100644
index 00000000000..a222db1d4ae
--- /dev/null
+++ b/server/src/main/java/org/apache/seata/server/cluster/raft/RaftConfigInitializer.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.server.cluster.raft;
+
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+
+@Order(Ordered.HIGHEST_PRECEDENCE)
+public class RaftConfigInitializer implements ApplicationContextInitializer {
+ @Override
+ public void initialize(ConfigurableApplicationContext applicationContext) {
+ RaftConfigServerManager.init();
+ RaftConfigServerManager.start();
+ }
+}
diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/RaftConfigServer.java b/server/src/main/java/org/apache/seata/server/cluster/raft/RaftConfigServer.java
new file mode 100644
index 00000000000..faf0e5ba9ef
--- /dev/null
+++ b/server/src/main/java/org/apache/seata/server/cluster/raft/RaftConfigServer.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.server.cluster.raft;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import com.alipay.sofa.jraft.Node;
+import com.alipay.sofa.jraft.RaftGroupService;
+import com.alipay.sofa.jraft.RouteTable;
+import com.alipay.sofa.jraft.entity.PeerId;
+import com.alipay.sofa.jraft.option.NodeOptions;
+import com.alipay.sofa.jraft.rpc.RpcServer;
+import com.codahale.metrics.Slf4jReporter;
+import org.apache.commons.io.FileUtils;
+import org.apache.seata.config.ConfigurationFactory;
+import org.apache.seata.core.rpc.Disposable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.seata.common.ConfigurationKeys.SERVER_RAFT_REPORTER_ENABLED;
+import static org.apache.seata.common.ConfigurationKeys.SERVER_RAFT_REPORTER_INITIAL_DELAY;
+
+public class RaftConfigServer implements Disposable, Closeable {
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+ private final RaftConfigStateMachine raftStateMachine;
+ private final String groupId;
+ private final String groupPath;
+ private final NodeOptions nodeOptions;
+ private final PeerId serverId;
+ private final RpcServer rpcServer;
+ private RaftGroupService raftGroupService;
+ private Node node;
+
+ public RaftConfigServer(final String dataPath, final String groupId, final PeerId serverId, final NodeOptions nodeOptions, final RpcServer rpcServer)
+ throws IOException {
+ this.groupId = groupId;
+ this.groupPath = dataPath + File.separator + groupId;
+ // Initialize the state machine
+ this.raftStateMachine = new RaftConfigStateMachine(groupId);
+ this.nodeOptions = nodeOptions;
+ this.serverId = serverId;
+ this.rpcServer = rpcServer;
+ }
+
+ public void start() throws IOException {
+ // Initialization path
+ FileUtils.forceMkdir(new File(groupPath));
+ // Set the state machine to startup parameters
+ nodeOptions.setFsm(this.raftStateMachine);
+ // Set the storage path
+ // Log, must
+ nodeOptions.setLogUri(groupPath + File.separator + "log");
+ // Meta information, must
+ nodeOptions.setRaftMetaUri(groupPath + File.separator + "raft_meta");
+ // Snapshot, optional, is generally recommended
+ nodeOptions.setSnapshotUri(groupPath + File.separator + "snapshot");
+ boolean reporterEnabled = ConfigurationFactory.CURRENT_FILE_INSTANCE.getBoolean(SERVER_RAFT_REPORTER_ENABLED, false);
+ nodeOptions.setEnableMetrics(reporterEnabled);
+ // Initialize the raft Group service framework
+ this.raftGroupService = new RaftGroupService(groupId, serverId, nodeOptions, rpcServer, true);
+ this.node = this.raftGroupService.start(false);
+ RouteTable.getInstance().updateConfiguration(groupId, node.getOptions().getInitialConf());
+ if (reporterEnabled) {
+ final Slf4jReporter reporter = Slf4jReporter.forRegistry(node.getNodeMetrics().getMetricRegistry())
+ .outputTo(logger).convertRatesTo(TimeUnit.SECONDS)
+ .convertDurationsTo(TimeUnit.MILLISECONDS).build();
+ reporter.start(ConfigurationFactory.CURRENT_FILE_INSTANCE.getInt(SERVER_RAFT_REPORTER_INITIAL_DELAY, 60),
+ TimeUnit.MINUTES);
+ }
+ }
+
+ public Node getNode() {
+ return this.node;
+ }
+
+
+ public RaftConfigStateMachine getRaftStateMachine() {
+ return raftStateMachine;
+ }
+
+ public PeerId getServerId() {
+ return serverId;
+ }
+
+ @Override
+ public void close() {
+ destroy();
+ }
+
+ @Override
+ public void destroy() {
+ Optional.ofNullable(raftGroupService).ifPresent(r -> {
+ r.shutdown();
+ try {
+ r.join();
+ } catch (InterruptedException e) {
+ logger.warn("Interrupted when RaftServer destroying", e);
+ }
+ });
+ }
+}
diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/RaftConfigServerManager.java b/server/src/main/java/org/apache/seata/server/cluster/raft/RaftConfigServerManager.java
new file mode 100644
index 00000000000..8413d2a237c
--- /dev/null
+++ b/server/src/main/java/org/apache/seata/server/cluster/raft/RaftConfigServerManager.java
@@ -0,0 +1,259 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.server.cluster.raft;
+
+import java.io.IOException;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.alipay.remoting.serialization.SerializerManager;
+import com.alipay.sofa.jraft.CliService;
+import com.alipay.sofa.jraft.RaftServiceFactory;
+import com.alipay.sofa.jraft.RouteTable;
+import com.alipay.sofa.jraft.conf.Configuration;
+import com.alipay.sofa.jraft.entity.PeerId;
+import com.alipay.sofa.jraft.option.CliOptions;
+import com.alipay.sofa.jraft.option.NodeOptions;
+import com.alipay.sofa.jraft.option.RaftOptions;
+import com.alipay.sofa.jraft.rpc.CliClientService;
+import com.alipay.sofa.jraft.rpc.RaftRpcServerFactory;
+import com.alipay.sofa.jraft.rpc.RpcServer;
+import com.alipay.sofa.jraft.rpc.impl.cli.CliClientServiceImpl;
+import org.apache.seata.common.ConfigurationKeys;
+import org.apache.seata.common.XID;
+import org.apache.seata.common.util.NetUtil;
+import org.apache.seata.common.util.StringUtils;
+import org.apache.seata.config.ConfigType;
+import org.apache.seata.config.ConfigurationFactory;
+import org.apache.seata.config.store.ConfigStoreManagerFactory;
+import org.apache.seata.core.serializer.SerializerType;
+import org.apache.seata.server.ServerRunner;
+import org.apache.seata.server.cluster.raft.processor.ConfigOperationRequestProcessor;
+import org.apache.seata.server.cluster.raft.processor.PutNodeInfoRequestProcessor;
+import org.apache.seata.server.cluster.raft.serializer.JacksonBoltSerializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static java.io.File.separator;
+import static org.apache.seata.common.ConfigurationKeys.CONFIG_STORE_DIR;
+import static org.apache.seata.common.ConfigurationKeys.SERVER_RAFT_APPLY_BATCH;
+import static org.apache.seata.common.ConfigurationKeys.SERVER_RAFT_DISRUPTOR_BUFFER_SIZE;
+import static org.apache.seata.common.ConfigurationKeys.SERVER_RAFT_ELECTION_TIMEOUT_MS;
+import static org.apache.seata.common.ConfigurationKeys.SERVER_RAFT_MAX_APPEND_BUFFER_SIZE;
+import static org.apache.seata.common.ConfigurationKeys.SERVER_RAFT_MAX_REPLICATOR_INFLIGHT_MSGS;
+import static org.apache.seata.common.ConfigurationKeys.SERVER_RAFT_PORT_CAMEL;
+import static org.apache.seata.common.ConfigurationKeys.SERVER_RAFT_SNAPSHOT_INTERVAL;
+import static org.apache.seata.common.ConfigurationKeys.SERVER_RAFT_SYNC;
+import static org.apache.seata.common.Constants.RAFT_CONFIG_GROUP;
+import static org.apache.seata.common.DefaultValues.DEFAULT_DB_STORE_FILE_DIR;
+import static org.apache.seata.common.DefaultValues.DEFAULT_SERVER_RAFT_ELECTION_TIMEOUT_MS;
+import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.REGEX_SPLIT_CHAR;
+import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.REGISTRY_PREFERED_NETWORKS;
+
+/**
+ * The type to manager raft server of config center
+ */
+public class RaftConfigServerManager {
+ private static final Logger LOGGER = LoggerFactory.getLogger(RaftConfigServerManager.class);
+ private static final AtomicBoolean INIT = new AtomicBoolean(false);
+ private static final org.apache.seata.config.Configuration CONFIG = ConfigurationFactory.CURRENT_FILE_INSTANCE;
+ private static RpcServer rpcServer;
+ private static RaftConfigServer raftServer;
+ private static volatile boolean RAFT_MODE;
+ private static final String GROUP = RAFT_CONFIG_GROUP;
+
+ public static CliService getCliServiceInstance() {
+ return RaftConfigServerManager.SingletonHandler.CLI_SERVICE;
+ }
+
+ public static CliClientService getCliClientServiceInstance() {
+ return RaftConfigServerManager.SingletonHandler.CLI_CLIENT_SERVICE;
+ }
+
+ public static void init() {
+ if (INIT.compareAndSet(false, true)) {
+ String initConfStr = CONFIG.getConfig(ConfigurationKeys.SERVER_RAFT_SERVER_ADDR);
+ String configTypeName = CONFIG.getConfig(org.apache.seata.config.ConfigurationKeys.FILE_ROOT_CONFIG
+ + org.apache.seata.config.ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR + org.apache.seata.config.ConfigurationKeys.FILE_ROOT_TYPE);
+ RAFT_MODE = ConfigType.Raft.name().equalsIgnoreCase(configTypeName);
+ if (!RAFT_MODE) {
+ return;
+ }
+ if (StringUtils.isBlank(initConfStr)) {
+ if (RAFT_MODE) {
+ throw new IllegalArgumentException(
+ "Raft config mode must config: " + ConfigurationKeys.SERVER_RAFT_SERVER_ADDR);
+ }
+ return;
+ }
+ final Configuration initConf = new Configuration();
+ if (!initConf.parse(initConfStr)) {
+ throw new IllegalArgumentException("fail to parse initConf:" + initConfStr);
+ }
+ int port = Integer.parseInt(System.getProperty(SERVER_RAFT_PORT_CAMEL, "0"));
+ PeerId serverId = null;
+ // XID may be null when configuration center is not initialized.
+ String host = null;
+ if (XID.getIpAddress() == null) {
+ String preferredNetworks = CONFIG.getConfig(REGISTRY_PREFERED_NETWORKS);
+ host = StringUtils.isNotBlank(preferredNetworks) ? NetUtil.getLocalIp(preferredNetworks.split(REGEX_SPLIT_CHAR)) : NetUtil.getLocalIp();
+ } else {
+ host = XID.getIpAddress();
+ }
+ if (port <= 0) {
+ // Highly available deployments require different nodes
+ for (PeerId peer : initConf.getPeers()) {
+ if (StringUtils.equals(peer.getIp(), host)) {
+ if (serverId != null) {
+ throw new IllegalArgumentException(
+ "server.raft.cluster has duplicate ip, For local debugging, use -Dserver.raftPort to specify the raft port");
+ }
+ serverId = peer;
+ }
+ }
+ } else {
+ // Local debugging use
+ serverId = new PeerId(host, port);
+ }
+ final String dataPath = CONFIG.getConfig(CONFIG_STORE_DIR, DEFAULT_DB_STORE_FILE_DIR)
+ + separator + "raft" + separator + serverId.getPort();
+ try {
+ // Here you have raft RPC and business RPC using the same RPC server, and you can usually do this
+ // separately
+ SerializerManager.addSerializer(SerializerType.JACKSON.getCode(), new JacksonBoltSerializer());
+ rpcServer = RaftRpcServerFactory.createRaftRpcServer(serverId.getEndpoint());
+ raftServer = new RaftConfigServer(dataPath, GROUP, serverId, initNodeOptions(initConf), rpcServer);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("fail init raft cluster:" + e.getMessage(), e);
+ }
+ }
+ }
+ public static void start() {
+ if (!RAFT_MODE) {
+ return;
+ }
+ try {
+ if (raftServer != null) {
+ raftServer.start();
+ }
+ } catch (IOException e) {
+ LOGGER.error("start seata server raft cluster error, group: {} ", GROUP, e);
+ throw new RuntimeException(e);
+ }
+ LOGGER.info("started seata server raft cluster, group: {} ", GROUP);
+
+ if (rpcServer != null) {
+ rpcServer.registerProcessor(new PutNodeInfoRequestProcessor());
+ rpcServer.registerProcessor(new ConfigOperationRequestProcessor());
+ if (!rpcServer.init(null)) {
+ throw new RuntimeException("start raft node fail!");
+ }
+ }
+ // Make sure to close it at the end, as other components may still use the configuration, such as ShutdownWaitTime.
+ ServerRunner.addDisposable(() -> {
+ RaftConfigServerManager.destroy();
+ ConfigStoreManagerFactory.destroy();
+ });
+ }
+
+
+ public static void destroy() {
+ if (raftServer != null) {
+ raftServer.close();
+ }
+ LOGGER.info("closed seata server raft cluster, group: {} ", GROUP);
+ Optional.ofNullable(rpcServer).ifPresent(RpcServer::shutdown);
+ raftServer = null;
+ rpcServer = null;
+ RAFT_MODE = false;
+ INIT.set(false);
+ }
+ public static boolean isRaftMode() {
+ return RAFT_MODE;
+ }
+ public static RaftConfigServer getRaftServer() {
+ return raftServer;
+ }
+
+ public static RpcServer getRpcServer() {
+ return rpcServer;
+ }
+
+ public static boolean isLeader() {
+ AtomicReference stateMachine = new AtomicReference<>();
+ Optional.ofNullable(raftServer).ifPresent(raftConfigServer -> {
+ stateMachine.set(raftConfigServer.getRaftStateMachine());
+ });
+ RaftConfigStateMachine raftStateMachine = stateMachine.get();
+ return raftStateMachine != null && raftStateMachine.isLeader();
+ }
+
+ public static PeerId getLeader() {
+
+ RouteTable routeTable = RouteTable.getInstance();
+ try {
+ routeTable.refreshLeader(getCliClientServiceInstance(), RAFT_CONFIG_GROUP , 1000);
+ return routeTable.selectLeader(RAFT_CONFIG_GROUP);
+ } catch (Exception e) {
+ LOGGER.error("there is an exception to getting the leader address: {}", e.getMessage(), e);
+ }
+ return null;
+
+ }
+ private static RaftOptions initRaftOptions() {
+ RaftOptions raftOptions = new RaftOptions();
+ raftOptions.setApplyBatch(CONFIG.getInt(SERVER_RAFT_APPLY_BATCH, raftOptions.getApplyBatch()));
+ raftOptions.setMaxAppendBufferSize(
+ CONFIG.getInt(SERVER_RAFT_MAX_APPEND_BUFFER_SIZE, raftOptions.getMaxAppendBufferSize()));
+ raftOptions.setDisruptorBufferSize(
+ CONFIG.getInt(SERVER_RAFT_DISRUPTOR_BUFFER_SIZE, raftOptions.getDisruptorBufferSize()));
+ raftOptions.setMaxReplicatorInflightMsgs(
+ CONFIG.getInt(SERVER_RAFT_MAX_REPLICATOR_INFLIGHT_MSGS, raftOptions.getMaxReplicatorInflightMsgs()));
+ raftOptions.setSync(CONFIG.getBoolean(SERVER_RAFT_SYNC, raftOptions.isSync()));
+ return raftOptions;
+ }
+
+ private static NodeOptions initNodeOptions(Configuration initConf) {
+ NodeOptions nodeOptions = new NodeOptions();
+ // enable the CLI service.
+ nodeOptions.setDisableCli(false);
+ // snapshot should be made every 600 seconds
+ int snapshotInterval = CONFIG.getInt(SERVER_RAFT_SNAPSHOT_INTERVAL, 60 * 10);
+ nodeOptions.setSnapshotIntervalSecs(snapshotInterval);
+ nodeOptions.setRaftOptions(initRaftOptions());
+ // set the election timeout to 1 second
+ nodeOptions
+ .setElectionTimeoutMs(CONFIG.getInt(SERVER_RAFT_ELECTION_TIMEOUT_MS, DEFAULT_SERVER_RAFT_ELECTION_TIMEOUT_MS));
+ // set up the initial cluster configuration
+ nodeOptions.setInitialConf(initConf);
+ return nodeOptions;
+ }
+ public static String getGroup() {
+ return GROUP;
+ }
+ private static class SingletonHandler {
+ private static final CliService CLI_SERVICE = RaftServiceFactory.createAndInitCliService(new CliOptions());
+ private static final CliClientService CLI_CLIENT_SERVICE = new CliClientServiceImpl();
+
+ static {
+ CLI_CLIENT_SERVICE.init(new CliOptions());
+ }
+
+ }
+
+}
diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/RaftConfigStateMachine.java b/server/src/main/java/org/apache/seata/server/cluster/raft/RaftConfigStateMachine.java
new file mode 100644
index 00000000000..4a9f48884da
--- /dev/null
+++ b/server/src/main/java/org/apache/seata/server/cluster/raft/RaftConfigStateMachine.java
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.server.cluster.raft;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
+
+import com.alipay.sofa.jraft.Closure;
+import com.alipay.sofa.jraft.Iterator;
+import com.alipay.sofa.jraft.RouteTable;
+import com.alipay.sofa.jraft.Status;
+import com.alipay.sofa.jraft.conf.Configuration;
+import com.alipay.sofa.jraft.core.StateMachineAdapter;
+import com.alipay.sofa.jraft.entity.LeaderChangeContext;
+import com.alipay.sofa.jraft.entity.PeerId;
+import com.alipay.sofa.jraft.rpc.InvokeContext;
+import com.alipay.sofa.jraft.rpc.impl.cli.CliClientServiceImpl;
+import com.alipay.sofa.jraft.storage.snapshot.SnapshotReader;
+import com.alipay.sofa.jraft.storage.snapshot.SnapshotWriter;
+import org.apache.seata.common.XID;
+import org.apache.seata.common.holder.ObjectHolder;
+import org.apache.seata.common.metadata.ClusterRole;
+import org.apache.seata.common.metadata.Node;
+import org.apache.seata.common.thread.NamedThreadFactory;
+import org.apache.seata.common.util.CollectionUtils;
+import org.apache.seata.common.util.NetUtil;
+import org.apache.seata.common.util.StringUtils;
+import org.apache.seata.core.serializer.SerializerType;
+import org.apache.seata.server.cluster.listener.ClusterChangeEvent;
+import org.apache.seata.server.cluster.raft.context.SeataClusterContext;
+import org.apache.seata.server.cluster.raft.execute.RaftMsgExecute;
+import org.apache.seata.server.cluster.raft.execute.config.ConfigOperationExecute;
+import org.apache.seata.server.cluster.raft.processor.request.PutNodeMetadataRequest;
+import org.apache.seata.server.cluster.raft.processor.response.ConfigOperationResponse;
+import org.apache.seata.server.cluster.raft.processor.response.PutNodeMetadataResponse;
+import org.apache.seata.server.cluster.raft.snapshot.StoreSnapshotFile;
+import org.apache.seata.server.cluster.raft.snapshot.config.ConfigSnapshotFile;
+import org.apache.seata.server.cluster.raft.snapshot.metadata.ConfigLeaderMetadataSnapshotFile;
+import org.apache.seata.server.cluster.raft.sync.RaftSyncMessageSerializer;
+import org.apache.seata.server.cluster.raft.sync.msg.RaftBaseMsg;
+import org.apache.seata.server.cluster.raft.sync.msg.RaftClusterMetadataMsg;
+import org.apache.seata.server.cluster.raft.sync.msg.RaftSyncMsgType;
+import org.apache.seata.server.cluster.raft.sync.msg.closure.ConfigClosure;
+import org.apache.seata.server.cluster.raft.sync.msg.dto.RaftClusterMetadata;
+import org.apache.seata.server.cluster.raft.util.RaftConfigTaskUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.core.env.Environment;
+
+import static org.apache.seata.common.Constants.OBJECT_KEY_SPRING_APPLICATION_CONTEXT;
+import static org.apache.seata.common.Constants.OBJECT_KEY_SPRING_CONFIGURABLE_ENVIRONMENT;
+import static org.apache.seata.server.cluster.raft.sync.msg.RaftSyncMsgType.CONFIG_OPERATION;
+import static org.apache.seata.server.cluster.raft.sync.msg.RaftSyncMsgType.REFRESH_CLUSTER_METADATA;
+
+
+/**
+ * The type raft config state machine.
+ */
+public class RaftConfigStateMachine extends StateMachineAdapter {
+ private static final Logger LOGGER = LoggerFactory.getLogger(RaftConfigStateMachine.class);
+
+ private final String group;
+
+ private final List snapshotFiles = new ArrayList<>();
+
+ private static final Map> EXECUTES = new HashMap<>();
+
+ private volatile RaftClusterMetadata raftClusterMetadata = new RaftClusterMetadata();
+
+ private final Lock lock = new ReentrantLock();
+
+ private static final ScheduledThreadPoolExecutor RESYNC_METADATA_POOL = new ScheduledThreadPoolExecutor(1, new NamedThreadFactory("reSyncMetadataPool", 1, true));
+
+ /**
+ * Leader term
+ */
+ private final AtomicLong leaderTerm = new AtomicLong(-1);
+
+ /**
+ * current term
+ */
+ private final AtomicLong currentTerm = new AtomicLong(-1);
+
+ private final AtomicBoolean initSync = new AtomicBoolean(false);
+
+ private ScheduledFuture> scheduledFuture;
+
+ public boolean isLeader() {
+ return this.leaderTerm.get() > 0;
+ }
+
+ public RaftConfigStateMachine(String group) {
+ this.group = group;
+
+ EXECUTES.put(REFRESH_CLUSTER_METADATA, syncMsg -> {
+ refreshClusterMetadata(syncMsg);
+ return null;
+ });
+ registryStoreSnapshotFile(new ConfigLeaderMetadataSnapshotFile(group));
+ registryStoreSnapshotFile(new ConfigSnapshotFile(group));
+ EXECUTES.put(CONFIG_OPERATION, new ConfigOperationExecute());
+ EXECUTES.put(REFRESH_CLUSTER_METADATA, syncMsg -> {
+ refreshClusterMetadata(syncMsg);
+ return null;
+ });
+ this.scheduledFuture =
+ RESYNC_METADATA_POOL.scheduleAtFixedRate(() -> syncCurrentNodeInfo(group), 10, 10, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void onApply(Iterator iterator) {
+ while (iterator.hasNext()) {
+ Closure done = iterator.done();
+ if (done != null) {
+ // leader does not need to be serialized, just execute the task directly
+ if (done instanceof ConfigClosure) {
+ ConfigClosure configClosure = (ConfigClosure) done;
+ RaftBaseMsg msg = configClosure.getRaftBaseMsg();
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("sync msg: {}", msg);
+ }
+ ConfigOperationResponse response = (ConfigOperationResponse) onExecuteRaft(msg);
+ configClosure.getResponse().setSuccess(response.isSuccess());
+ configClosure.getResponse().setResult(response.getResult());
+ configClosure.getResponse().setErrMsg(response.getErrMsg());
+ configClosure.run(Status.OK());
+ } else {
+ // If it's not a ConfigClosure, just run it with OK status
+ done.run(Status.OK());
+ }
+ } else {
+ ByteBuffer byteBuffer = iterator.getData();
+ // if data is empty, it is only a heartbeat event and can be ignored
+ if (byteBuffer != null && byteBuffer.hasRemaining()) {
+ RaftBaseMsg msg = (RaftBaseMsg) RaftSyncMessageSerializer.decode(byteBuffer.array()).getBody();
+ // follower executes the corresponding task
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("sync msg: {}", msg);
+ }
+ onExecuteRaft(msg);
+ }
+ }
+ iterator.next();
+ }
+ }
+
+ @Override
+ public void onSnapshotSave(final SnapshotWriter writer, final Closure done) {
+ long current = System.currentTimeMillis();
+ for (StoreSnapshotFile snapshotFile : snapshotFiles) {
+ Status status = snapshotFile.save(writer);
+ if (!status.isOk()) {
+ done.run(status);
+ return;
+ }
+ }
+ LOGGER.info("groupId: {}, onSnapshotSave cost: {} ms.", group, System.currentTimeMillis() - current);
+ done.run(Status.OK());
+ }
+
+ @Override
+ public boolean onSnapshotLoad(final SnapshotReader reader) {
+ if (isLeader()) {
+ if (LOGGER.isWarnEnabled()) {
+ LOGGER.warn("Leader is not supposed to load snapshot");
+ }
+ return false;
+ }
+ long current = System.currentTimeMillis();
+ for (StoreSnapshotFile snapshotFile : snapshotFiles) {
+ if (!snapshotFile.load(reader)) {
+ return false;
+ }
+ }
+ LOGGER.info("groupId: {}, onSnapshotLoad cost: {} ms.", group, System.currentTimeMillis() - current);
+ return true;
+ }
+ @Override
+ public void onLeaderStart(final long term) {
+ boolean leader = isLeader();
+ this.leaderTerm.set(term);
+ LOGGER.info("groupId: {}, onLeaderStart: term={}.", group, term);
+ this.currentTerm.set(term);
+ syncMetadata();
+ if (!leader && RaftConfigServerManager.isRaftMode()) {
+ Configuration conf = RouteTable.getInstance().getConfiguration(group);
+ // A member change might trigger a leader re-election. At this point, it’s necessary to filter out non-existent members and synchronize again.
+ changePeers(conf);
+ }
+ }
+
+ @Override
+ public void onLeaderStop(final Status status) {
+ this.leaderTerm.set(-1);
+ LOGGER.info("groupId: {}, onLeaderStop: status={}.", group, status);
+ }
+
+ @Override
+ public void onStopFollowing(final LeaderChangeContext ctx) {
+ LOGGER.info("groupId: {}, onStopFollowing: {}.", group, ctx);
+ }
+
+ @Override
+ public void onStartFollowing(final LeaderChangeContext ctx) {
+ LOGGER.info("groupId: {}, onStartFollowing: {}.", group, ctx);
+ this.currentTerm.set(ctx.getTerm());
+ CompletableFuture.runAsync(() -> syncCurrentNodeInfo(ctx.getLeaderId()), RESYNC_METADATA_POOL);
+ }
+
+ @Override
+ public void onConfigurationCommitted(Configuration conf) {
+ LOGGER.info("groupId: {}, onConfigurationCommitted: {}.", group, conf);
+ RouteTable.getInstance().updateConfiguration(group, conf);
+ // After a member change, the metadata needs to be synchronized again.
+ initSync.compareAndSet(true, false);
+ if (isLeader()) {
+ changePeers(conf);
+ }
+ }
+ private void changePeers(Configuration conf) {
+ lock.lock();
+ try {
+ List newFollowers = conf.getPeers();
+ Set newLearners = conf.getLearners();
+ List currentFollowers = raftClusterMetadata.getFollowers();
+ if (CollectionUtils.isNotEmpty(newFollowers)) {
+ raftClusterMetadata.setFollowers(currentFollowers.stream().filter(node -> contains(node, newFollowers))
+ .collect(Collectors.toList()));
+ }
+ if (CollectionUtils.isNotEmpty(newLearners)) {
+ raftClusterMetadata.setLearner(raftClusterMetadata.getLearner().stream()
+ .filter(node -> contains(node, newLearners)).collect(Collectors.toList()));
+ } else {
+ raftClusterMetadata.setLearner(Collections.emptyList());
+ }
+ CompletableFuture.runAsync(this::syncMetadata, RESYNC_METADATA_POOL);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private boolean contains(Node node, Collection list) {
+ // This indicates that the node is of a lower version.
+ // When scaling up or down on a higher version
+ // you need to ensure that the cluster is consistent first
+ // otherwise, the lower version nodes may be removed.
+ if (node.getInternal() == null) {
+ return true;
+ }
+ PeerId nodePeer = new PeerId(node.getInternal().getHost(), node.getInternal().getPort());
+ return list.contains(nodePeer);
+ }
+
+ public void syncMetadata() {
+ if (isLeader()) {
+ SeataClusterContext.bindGroup(group);
+ try {
+ RaftClusterMetadataMsg raftClusterMetadataMsg =
+ new RaftClusterMetadataMsg(changeOrInitRaftClusterMetadata());
+ RaftConfigTaskUtil.createTask(status -> refreshClusterMetadata(raftClusterMetadataMsg),
+ raftClusterMetadataMsg, null);
+ } catch (Exception e) {
+ LOGGER.error(e.getMessage(), e);
+ } finally {
+ SeataClusterContext.unbindGroup();
+ }
+ }
+ }
+
+ private Object onExecuteRaft(RaftBaseMsg msg) {
+ RaftMsgExecute> execute = EXECUTES.get(msg.getMsgType());
+ if (execute == null) {
+ throw new RuntimeException(
+ "the state machine does not allow events that cannot be executed, please feedback the information to the Seata community !!! msg: "
+ + msg);
+ }
+ try {
+ return execute.execute(msg);
+ } catch (Throwable e) {
+ LOGGER.error("Message synchronization failure: {}, msgType: {}", e.getMessage(), msg.getMsgType(), e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public AtomicLong getCurrentTerm() {
+ return currentTerm;
+ }
+
+ public void registryStoreSnapshotFile(StoreSnapshotFile storeSnapshotFile) {
+ snapshotFiles.add(storeSnapshotFile);
+ }
+
+ public RaftClusterMetadata getRaftLeaderMetadata() {
+ return raftClusterMetadata;
+ }
+
+ public void setRaftLeaderMetadata(RaftClusterMetadata raftClusterMetadata) {
+ this.raftClusterMetadata = raftClusterMetadata;
+ }
+
+ public RaftClusterMetadata changeOrInitRaftClusterMetadata() {
+ raftClusterMetadata.setTerm(this.currentTerm.get());
+ Node leaderNode = raftClusterMetadata.getLeader();
+ RaftConfigServer raftServer = RaftConfigServerManager.getRaftServer();
+ PeerId cureentPeerId = raftServer.getServerId();
+ // After the re-election, the leader information may be different from the latest leader, and you need to replace the leader information
+ if (leaderNode == null || (leaderNode.getInternal() != null
+ && !cureentPeerId.equals(new PeerId(leaderNode.getInternal().getHost(), leaderNode.getInternal().getPort())))) {
+ Node leader =
+ raftClusterMetadata.createNode(XID.getIpAddress() == null ? NetUtil.getLocalIp() : XID.getIpAddress(), XID.getPort() <= 0 ? 8091 : XID.getPort(), raftServer.getServerId().getPort(),
+ Integer.parseInt(
+ ((Environment) ObjectHolder.INSTANCE.getObject(OBJECT_KEY_SPRING_CONFIGURABLE_ENVIRONMENT))
+ .getProperty("server.port", String.valueOf(7091))),
+ group, Collections.emptyMap());
+ leader.setRole(ClusterRole.LEADER);
+ raftClusterMetadata.setLeader(leader);
+ }
+ return raftClusterMetadata;
+ }
+
+ public void refreshClusterMetadata(RaftBaseMsg syncMsg) {
+ // Directly receive messages from the leader and update the cluster metadata
+ raftClusterMetadata = ((RaftClusterMetadataMsg)syncMsg).getRaftClusterMetadata();
+ if (ObjectHolder.INSTANCE.getObject(OBJECT_KEY_SPRING_APPLICATION_CONTEXT) != null) {
+ ((ApplicationEventPublisher)ObjectHolder.INSTANCE.getObject(OBJECT_KEY_SPRING_APPLICATION_CONTEXT))
+ .publishEvent(new ClusterChangeEvent(this, group, raftClusterMetadata.getTerm(), this.isLeader()));
+ LOGGER.info("groupId: {}, refresh cluster metadata: {}", group, raftClusterMetadata);
+ }
+
+ }
+
+ private void syncCurrentNodeInfo(String group) {
+ if (initSync.compareAndSet(false, true)) {
+ try {
+ RouteTable.getInstance().refreshLeader(RaftConfigServerManager.getCliClientServiceInstance(), group, 1000);
+ PeerId peerId = RouteTable.getInstance().selectLeader(group);
+ if (peerId != null) {
+ syncCurrentNodeInfo(peerId);
+ }
+ } catch (Exception e) {
+ LOGGER.error(e.getMessage(), e);
+ }
+ }
+ }
+
+ private void syncCurrentNodeInfo(PeerId leaderPeerId) {
+ try {
+ // Ensure that the current leader must be version 2.1 or later to synchronize the operation
+ Node leader = raftClusterMetadata.getLeader();
+ if (leader != null && StringUtils.isNotBlank(leader.getVersion())) {
+ RaftConfigServer raftServer = RaftConfigServerManager.getRaftServer();
+ PeerId cureentPeerId = raftServer.getServerId();
+ Node node = raftClusterMetadata.createNode(XID.getIpAddress() == null ? NetUtil.getLocalIp() : XID.getIpAddress(), XID.getPort() <= 0 ? 8091 : XID.getPort(), cureentPeerId.getPort(),
+ Integer.parseInt(
+ ((Environment)ObjectHolder.INSTANCE.getObject(OBJECT_KEY_SPRING_CONFIGURABLE_ENVIRONMENT))
+ .getProperty("server.port", String.valueOf(7091))),
+ group, Collections.emptyMap());
+ InvokeContext invokeContext = new InvokeContext();
+ PutNodeMetadataRequest putNodeInfoRequest = new PutNodeMetadataRequest(node);
+ Configuration configuration = RouteTable.getInstance().getConfiguration(group);
+ node.setRole(
+ configuration.getPeers().contains(cureentPeerId) ? ClusterRole.FOLLOWER : ClusterRole.LEARNER);
+ invokeContext.put(com.alipay.remoting.InvokeContext.BOLT_CUSTOM_SERIALIZER,
+ SerializerType.JACKSON.getCode());
+ CliClientServiceImpl cliClientService =
+ (CliClientServiceImpl)RaftConfigServerManager.getCliClientServiceInstance();
+ // The previous leader may be an old snapshot or log playback, which is not accurate, and you
+ // need to get the leader again
+ cliClientService.getRpcClient().invokeAsync(leaderPeerId.getEndpoint(), putNodeInfoRequest,
+ invokeContext, (result, err) -> {
+ if (err == null) {
+ PutNodeMetadataResponse putNodeMetadataResponse = (PutNodeMetadataResponse)result;
+ if (putNodeMetadataResponse.isSuccess()) {
+ scheduledFuture.cancel(true);
+ LOGGER.info("sync node info to leader: {}, result: {}", leaderPeerId, result);
+ } else {
+ initSync.compareAndSet(true, false);
+ LOGGER.info(
+ "sync node info to leader: {}, result: {}, retry will be made at the time of the re-election or after 10 seconds",
+ leaderPeerId, result);
+ }
+ } else {
+ initSync.compareAndSet(true, false);
+ LOGGER.error("sync node info to leader: {}, error: {}", leaderPeerId, err.getMessage(),
+ err);
+ }
+ }, 30000);
+ }
+ } catch (Exception e) {
+ LOGGER.error(e.getMessage(), e);
+ }
+ }
+
+ public void changeNodeMetadata(Node node) {
+ lock.lock();
+ try {
+ List list = node.getRole() == ClusterRole.FOLLOWER ? raftClusterMetadata.getFollowers()
+ : raftClusterMetadata.getLearner();
+ // If the node currently exists, modify it
+ for (Node follower : list) {
+ Node.Endpoint endpoint = follower.getInternal();
+ if (endpoint != null) {
+ // change old follower node metadata
+ if (endpoint.getHost().equals(node.getInternal().getHost())
+ && endpoint.getPort() == node.getInternal().getPort()) {
+ follower.setTransaction(node.getTransaction());
+ follower.setControl(node.getControl());
+ follower.setGroup(group);
+ follower.setMetadata(node.getMetadata());
+ follower.setVersion(node.getVersion());
+ follower.setRole(node.getRole());
+ return;
+ }
+ }
+ }
+ // add new node node metadata
+ list.add(node);
+ syncMetadata();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+
+}
diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/RaftServerManager.java b/server/src/main/java/org/apache/seata/server/cluster/raft/RaftServerManager.java
index e5bfcb1056e..845941bc349 100644
--- a/server/src/main/java/org/apache/seata/server/cluster/raft/RaftServerManager.java
+++ b/server/src/main/java/org/apache/seata/server/cluster/raft/RaftServerManager.java
@@ -135,7 +135,7 @@ public static void init() {
try {
// Here you have raft RPC and business RPC using the same RPC server, and you can usually do this
// separately
- rpcServer = RaftRpcServerFactory.createRaftRpcServer(serverId.getEndpoint());
+ rpcServer = RaftConfigServerManager.getRpcServer() == null ? RaftRpcServerFactory.createRaftRpcServer(serverId.getEndpoint()) : RaftConfigServerManager.getRpcServer();
RaftServer raftServer = new RaftServer(dataPath, group, serverId, initNodeOptions(initConf), rpcServer);
// as the foundation for multi raft group in the future
RAFT_SERVER_MAP.put(group, raftServer);
@@ -155,7 +155,7 @@ public static void start() {
}
LOGGER.info("started seata server raft cluster, group: {} ", group);
});
- if (rpcServer != null) {
+ if (rpcServer != null && RaftConfigServerManager.getRpcServer() == null) {
rpcServer.registerProcessor(new PutNodeInfoRequestProcessor());
SerializerManager.addSerializer(SerializerType.JACKSON.getCode(), new JacksonBoltSerializer());
if (!rpcServer.init(null)) {
diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/execute/config/AbstractRaftConfigMsgExecute.java b/server/src/main/java/org/apache/seata/server/cluster/raft/execute/config/AbstractRaftConfigMsgExecute.java
new file mode 100644
index 00000000000..4ce6b31e342
--- /dev/null
+++ b/server/src/main/java/org/apache/seata/server/cluster/raft/execute/config/AbstractRaftConfigMsgExecute.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.server.cluster.raft.execute.config;
+
+import org.apache.seata.config.store.ConfigStoreManager;
+import org.apache.seata.config.store.ConfigStoreManagerFactory;
+import org.apache.seata.server.cluster.raft.execute.RaftMsgExecute;
+
+
+
+public abstract class AbstractRaftConfigMsgExecute implements RaftMsgExecute