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 */} +
+ {/* {search filters} */} + + { this.searchFilterOnChange('dataId', value); }} + dataSource={this.state.dataIds} + style={{ width: 200 }} + hasClear={true} + /> + + + {/* {reset search filter button} */} + + + {resetButtonLabel} + + + {/* {search button} */} + + + {searchButtonLabel} + + + + + {createButtonLabel} + + + + + {uploadButtonLabel} + + + + + {clearButtonLabel} + + +
+ + {/* config info table */} +
+ + + + + + ( + <> + + + + )} + /> +
+
+ {/* config edit dialog */} + +
+ + this.handleDialogInputChange('namespace', value)} + /> + + + this.handleDialogInputChange('dataId', value)} + /> + + + this.handleDialogInputChange('key', value)} + /> + + + this.handleDialogInputChange('value', value)} + /> + +
+
+ + {/* config delete dialog*/} + +
+ + + { + this.handleDeleteDialogInputChange('namespace', value)} + } + /> + + + { + this.handleDeleteDialogInputChange('dataId', value)} + } + /> + +
+
+ + {/* config upload dialog*/} + +
+ + + this.handleUploadDialogInputChange('namespace', value)} + /> + + + this.handleUploadDialogInputChange('dataId', value)} + /> + + + false} // Prevent auto-upload + accept={".txt,.text,.yaml,.yml,.properties"} + limit={1} + > + + + +
+
+
+ ); + } +} +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 { + + protected ConfigStoreManager configStoreManager = ConfigStoreManagerFactory.getInstance(); + +} diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/execute/config/ConfigOperationExecute.java b/server/src/main/java/org/apache/seata/server/cluster/raft/execute/config/ConfigOperationExecute.java new file mode 100644 index 00000000000..9ebdb937b4b --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/execute/config/ConfigOperationExecute.java @@ -0,0 +1,143 @@ +/* + * 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 java.util.List; +import java.util.Map; + +import org.apache.seata.common.holder.ObjectHolder; +import org.apache.seata.config.dto.ConfigurationInfoDto; +import org.apache.seata.server.cluster.listener.ClusterConfigChangeEvent; +import org.apache.seata.server.cluster.raft.processor.response.ConfigOperationResponse; +import org.apache.seata.server.cluster.raft.sync.msg.RaftBaseMsg; +import org.apache.seata.server.cluster.raft.sync.msg.RaftConfigOperationSyncMsg; +import org.apache.seata.server.cluster.raft.sync.msg.dto.ConfigOperationDTO; +import org.apache.seata.config.dto.ConfigurationItem; +import org.apache.seata.server.config.ConfigurationProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; + +import static org.apache.seata.common.Constants.OBJECT_KEY_SPRING_APPLICATION_CONTEXT; + + +public class ConfigOperationExecute extends AbstractRaftConfigMsgExecute { + private static final Logger LOGGER = LoggerFactory.getLogger(ConfigOperationExecute.class); + + @Override + public Object execute(RaftBaseMsg syncMsg) throws Throwable { + RaftConfigOperationSyncMsg configSyncMsg = (RaftConfigOperationSyncMsg) syncMsg; + ConfigOperationDTO configOperation = configSyncMsg.getConfigOperation(); + switch (configOperation.getOptType()) { + case PUT: + return put(configOperation); + case DELETE: + return delete(configOperation); + case DELETE_ALL: + return deleteAll(configOperation); + case GET: + return get(configOperation); + case GET_ALL: + return getAll(configOperation); + case UPLOAD: + return upload(configOperation); + case GET_NAMESPACES: + return getNamespaces(configOperation); + case GET_DATA_IDS: + return getDataIds(configOperation); + default: + return ConfigOperationResponse.fail("unknown operation type"); + } + } + + private ConfigOperationResponse get(ConfigOperationDTO configOperation) { + String result = configStoreManager.get(configOperation.getNamespace(), configOperation.getDataId(), configOperation.getKey()); + // fill config description and default value + ConfigurationItem item = ConfigurationProcessor.processConfigItem(configOperation.getKey(), result); + return ConfigOperationResponse.success(item); + } + + private ConfigOperationResponse put(ConfigOperationDTO configOperation) { + Boolean success = configStoreManager.put(configOperation.getNamespace(), configOperation.getDataId(), configOperation.getKey(), configOperation.getValue()); + if (success) { + // ApplicationContext may not have been started at this point + if (ObjectHolder.INSTANCE.getObject(OBJECT_KEY_SPRING_APPLICATION_CONTEXT) != null) { + ((ApplicationEventPublisher) ObjectHolder.INSTANCE.getObject(OBJECT_KEY_SPRING_APPLICATION_CONTEXT)) + .publishEvent(new ClusterConfigChangeEvent(this, configOperation.getNamespace(), configOperation.getDataId())); + } + LOGGER.info("config namespace: {}, dataId: {}, config change event: {}", configOperation.getNamespace(), configOperation.getDataId(), configOperation.getOptType()); + } + return success ? ConfigOperationResponse.success() : ConfigOperationResponse.fail(); + } + + private ConfigOperationResponse delete(ConfigOperationDTO configOperation) { + Boolean success = configStoreManager.delete(configOperation.getNamespace(), configOperation.getDataId(), configOperation.getKey()); + if (success) { + if (ObjectHolder.INSTANCE.getObject(OBJECT_KEY_SPRING_APPLICATION_CONTEXT) != null) { + ((ApplicationEventPublisher) ObjectHolder.INSTANCE.getObject(OBJECT_KEY_SPRING_APPLICATION_CONTEXT)) + .publishEvent(new ClusterConfigChangeEvent(this, configOperation.getNamespace(), configOperation.getDataId())); + } + LOGGER.info("config namespace: {}, dataId: {}, config change event: {}", configOperation.getNamespace(), configOperation.getDataId(), configOperation.getOptType()); + } + return success ? ConfigOperationResponse.success() : ConfigOperationResponse.fail(); + } + + private ConfigOperationResponse deleteAll(ConfigOperationDTO configOperation) { + Boolean success = configStoreManager.deleteAll(configOperation.getNamespace(), configOperation.getDataId()); + if (success) { + if (ObjectHolder.INSTANCE.getObject(OBJECT_KEY_SPRING_APPLICATION_CONTEXT) != null) { + ((ApplicationEventPublisher) ObjectHolder.INSTANCE.getObject(OBJECT_KEY_SPRING_APPLICATION_CONTEXT)) + .publishEvent(new ClusterConfigChangeEvent(this, configOperation.getNamespace(), configOperation.getDataId())); + } + LOGGER.info("config namespace: {}, dataId: {}, config change event: {}", configOperation.getNamespace(), configOperation.getDataId(), configOperation.getOptType()); + } + return success ? ConfigOperationResponse.success() : ConfigOperationResponse.fail(); + } + + private ConfigOperationResponse upload(ConfigOperationDTO configOperation) { + Boolean success = configStoreManager.putAll(configOperation.getNamespace(), configOperation.getDataId(), (Map) configOperation.getValue()); + if (success) { + if (ObjectHolder.INSTANCE.getObject(OBJECT_KEY_SPRING_APPLICATION_CONTEXT) != null) { + ((ApplicationEventPublisher) ObjectHolder.INSTANCE.getObject(OBJECT_KEY_SPRING_APPLICATION_CONTEXT)) + .publishEvent(new ClusterConfigChangeEvent(this, configOperation.getNamespace(), configOperation.getDataId())); + } + LOGGER.info("config namespace: {}, dataId: {}, config change event: {}", configOperation.getNamespace(), configOperation.getDataId(), configOperation.getOptType()); + } + return success ? ConfigOperationResponse.success() : ConfigOperationResponse.fail(); + } + + private ConfigOperationResponse getAll(ConfigOperationDTO configOperation) { + Map configMap = configStoreManager.getAll(configOperation.getNamespace(), configOperation.getDataId()); + Long configVersion = configStoreManager.getConfigVersion(configOperation.getNamespace(), configOperation.getDataId()); + // fill config description and default value + Map itemMap = ConfigurationProcessor.processConfigMap(configMap); + ConfigurationInfoDto configurationInfoDto = new ConfigurationInfoDto(); + configurationInfoDto.setConfig(itemMap); + configurationInfoDto.setVersion(configVersion); + return ConfigOperationResponse.success(configurationInfoDto); + } + + private ConfigOperationResponse getNamespaces(ConfigOperationDTO configOperation) { + List namespaces = configStoreManager.getAllNamespaces(); + return ConfigOperationResponse.success(namespaces); + } + + private ConfigOperationResponse getDataIds(ConfigOperationDTO configOperation) { + List dataIds = configStoreManager.getAllDataIds(configOperation.getNamespace()); + return ConfigOperationResponse.success(dataIds); + } +} diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/execute/config/ConfigOperationType.java b/server/src/main/java/org/apache/seata/server/cluster/raft/execute/config/ConfigOperationType.java new file mode 100644 index 00000000000..8d38613d43d --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/execute/config/ConfigOperationType.java @@ -0,0 +1,70 @@ +/* + * 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; + +public enum ConfigOperationType { + + /** + * Get configuration operation + */ + GET("get"), + + /** + * Put configuration operation + */ + PUT("put"), + + /** + * Delete configuration operation + */ + DELETE("delete"), + + /** + * Delete all configuration operation + */ + DELETE_ALL("deleteAll"), + + /** + * Upload configuration operation + */ + UPLOAD("upload"), + + /** + * Get all configuration operation + */ + GET_ALL("getAll"), + + /** + * Get namespaces operation + */ + GET_NAMESPACES("getNamespaces"), + + /** + * Get data ids operation + */ + GET_DATA_IDS("getDataIds"); + + private final String type; + + ConfigOperationType(String type) { + this.type = type; + } + + public String getType() { + return type; + } +} diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/processor/ConfigOperationRequestProcessor.java b/server/src/main/java/org/apache/seata/server/cluster/raft/processor/ConfigOperationRequestProcessor.java new file mode 100644 index 00000000000..8cc1f2abe62 --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/processor/ConfigOperationRequestProcessor.java @@ -0,0 +1,67 @@ +/* + * 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.processor; + +import com.alipay.sofa.jraft.rpc.RpcContext; +import com.alipay.sofa.jraft.rpc.RpcProcessor; +import org.apache.seata.core.exception.TransactionException; +import org.apache.seata.server.cluster.raft.RaftConfigServerManager; +import org.apache.seata.server.cluster.raft.processor.request.ConfigOperationRequest; +import org.apache.seata.server.cluster.raft.processor.response.ConfigOperationResponse; +import org.apache.seata.server.cluster.raft.sync.msg.RaftConfigOperationSyncMsg; +import org.apache.seata.server.cluster.raft.sync.msg.closure.ConfigClosure; +import org.apache.seata.server.cluster.raft.sync.msg.dto.ConfigOperationDTO; +import org.apache.seata.server.cluster.raft.util.RaftConfigTaskUtil; + + +public class ConfigOperationRequestProcessor implements RpcProcessor { + private static final String NOT_LEADER = "not leader"; + @Override + public void handleRequest(RpcContext rpcCtx, ConfigOperationRequest request) { + if (RaftConfigServerManager.isLeader()) { + onExecute(rpcCtx, request); + } else { + rpcCtx.sendResponse(ConfigOperationResponse.fail(NOT_LEADER)); + } + } + + private void onExecute(RpcContext rpcCtx, ConfigOperationRequest request) { + ConfigOperationDTO operationDTO = ConfigOperationDTO.convertConfigRequest2Dto(request); + RaftConfigOperationSyncMsg syncMsg = new RaftConfigOperationSyncMsg(operationDTO); + ConfigOperationResponse response = new ConfigOperationResponse(); + ConfigClosure closure = new ConfigClosure(); + closure.setRaftBaseMsg(syncMsg); + closure.setResponse(response); + closure.setDone(status -> { + if (!status.isOk()) { + response.setSuccess(false); + response.setErrMsg(status.getErrorMsg()); + } + rpcCtx.sendResponse(response); + }); + try { + RaftConfigTaskUtil.createTask(closure, syncMsg, null); + } catch (TransactionException e) { + throw new RuntimeException(e); + } + } + + @Override + public String interest() { + return ConfigOperationRequest.class.getName(); + } +} diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/processor/PutNodeInfoRequestProcessor.java b/server/src/main/java/org/apache/seata/server/cluster/raft/processor/PutNodeInfoRequestProcessor.java index de90cbd8c5d..41e060fad60 100644 --- a/server/src/main/java/org/apache/seata/server/cluster/raft/processor/PutNodeInfoRequestProcessor.java +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/processor/PutNodeInfoRequestProcessor.java @@ -19,12 +19,16 @@ import com.alipay.sofa.jraft.rpc.RpcContext; import com.alipay.sofa.jraft.rpc.RpcProcessor; import org.apache.seata.common.metadata.Node; +import org.apache.seata.server.cluster.raft.RaftConfigServer; +import org.apache.seata.server.cluster.raft.RaftConfigServerManager; +import org.apache.seata.server.cluster.raft.RaftConfigStateMachine; import org.apache.seata.server.cluster.raft.RaftServer; import org.apache.seata.server.cluster.raft.RaftServerManager; import org.apache.seata.server.cluster.raft.RaftStateMachine; import org.apache.seata.server.cluster.raft.processor.request.PutNodeMetadataRequest; import org.apache.seata.server.cluster.raft.processor.response.PutNodeMetadataResponse; + public class PutNodeInfoRequestProcessor implements RpcProcessor { public PutNodeInfoRequestProcessor() { @@ -35,6 +39,25 @@ public PutNodeInfoRequestProcessor() { public void handleRequest(RpcContext rpcCtx, PutNodeMetadataRequest request) { Node node = request.getNode(); String group = node.getGroup(); + if (RaftConfigServerManager.getGroup().equals(group)) { + changeConfigGroupRequest(group, node, rpcCtx, request); + } else { + changeNormalGroupRequest(group, node, rpcCtx, request); + } + } + + private static void changeConfigGroupRequest(String group, Node node, RpcContext rpcCtx, PutNodeMetadataRequest request) { + if (RaftConfigServerManager.isLeader()) { + RaftConfigServer raftServer = RaftConfigServerManager.getRaftServer(); + RaftConfigStateMachine raftStateMachine = raftServer.getRaftStateMachine(); + raftStateMachine.changeNodeMetadata(node); + rpcCtx.sendResponse(new PutNodeMetadataResponse(true)); + } else { + rpcCtx.sendResponse(new PutNodeMetadataResponse(false)); + } + } + + private static void changeNormalGroupRequest(String group, Node node, RpcContext rpcCtx, PutNodeMetadataRequest request) { if (RaftServerManager.isLeader(group)) { RaftServer raftServer = RaftServerManager.getRaftServer(group); RaftStateMachine raftStateMachine = raftServer.getRaftStateMachine(); diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/processor/request/ConfigOperationRequest.java b/server/src/main/java/org/apache/seata/server/cluster/raft/processor/request/ConfigOperationRequest.java new file mode 100644 index 00000000000..d0ace0b2a50 --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/processor/request/ConfigOperationRequest.java @@ -0,0 +1,137 @@ +/* + * 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.processor.request; + +import java.io.Serializable; +import java.util.Map; + +import org.apache.seata.server.cluster.raft.execute.config.ConfigOperationType; + +public class ConfigOperationRequest implements Serializable { + private static final long serialVersionUID = -1149573667621259458L; + private ConfigOperationType optType; + private String namespace; + private String dataId; + private String key; + private Object value; + + public ConfigOperationRequest() { + } + + public ConfigOperationRequest(ConfigOperationType optType, String namespace, String dataId) { + this.optType = optType; + this.namespace = namespace; + this.dataId = dataId; + } + + public ConfigOperationRequest(ConfigOperationType optType,String namespace, String dataId, String key) { + this.optType = optType; + this.namespace = namespace; + this.dataId = dataId; + this.key = key; + } + + public ConfigOperationRequest(ConfigOperationType optType, String namespace, String dataId, String key, Object value) { + this.optType = optType; + this.namespace = namespace; + this.dataId = dataId; + this.key = key; + this.value = value; + } + + public static ConfigOperationRequest buildGetRequest(String namespace, String dataId, String key) { + return new ConfigOperationRequest(ConfigOperationType.GET, namespace, dataId, key); + } + + public static ConfigOperationRequest buildPutRequest(String namespace, String dataId, String key, String value) { + return new ConfigOperationRequest(ConfigOperationType.PUT, namespace, dataId, key, value); + } + + public static ConfigOperationRequest buildDeleteRequest(String namespace, String dataId, String key) { + return new ConfigOperationRequest(ConfigOperationType.DELETE, namespace, dataId, key); + } + public static ConfigOperationRequest buildDeleteAllRequest(String namespace, String dataId) { + return new ConfigOperationRequest(ConfigOperationType.DELETE_ALL, namespace, dataId); + } + + public static ConfigOperationRequest buildGetAllRequest(String namespace, String dataId) { + return new ConfigOperationRequest(ConfigOperationType.GET_ALL, namespace, dataId); + } + + public static ConfigOperationRequest buildUploadRequest(String namespace, String dataId, Map configMap) { + return new ConfigOperationRequest(ConfigOperationType.UPLOAD, namespace, dataId, null, configMap); + } + + public static ConfigOperationRequest buildGetNamespaces() { + return new ConfigOperationRequest(ConfigOperationType.GET_NAMESPACES, null, null); + } + + public static ConfigOperationRequest buildGetDataIds(String namespace) { + return new ConfigOperationRequest(ConfigOperationType.GET_DATA_IDS, namespace, null); + } + + + public ConfigOperationType getOptType() { + return optType; + } + public void setOptType(ConfigOperationType optType) { + this.optType = optType; + } + + 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; + } + + 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; + } + + @Override + public String toString() { + return "ConfigOperationRequest{" + + "optType=" + optType + + ", namespace='" + namespace + '\'' + + ", dataId='" + dataId + '\'' + + ", key='" + key + '\'' + + ", value=" + value + + '}'; + } +} diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/processor/response/ConfigOperationResponse.java b/server/src/main/java/org/apache/seata/server/cluster/raft/processor/response/ConfigOperationResponse.java new file mode 100644 index 00000000000..4b1a2222e11 --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/processor/response/ConfigOperationResponse.java @@ -0,0 +1,78 @@ +/* + * 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.processor.response; + +import java.io.Serializable; + +public class ConfigOperationResponse implements Serializable { + private static final long serialVersionUID = -1439073440621259777L; + + private Object result; + private boolean success; + private String errMsg; + + public ConfigOperationResponse() { + } + + public Object getResult() { + return result; + } + + public void setResult(Object result) { + this.result = result; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getErrMsg() { + return errMsg; + } + + public void setErrMsg(String errMsg) { + this.errMsg = errMsg; + } + + public static ConfigOperationResponse success() { + ConfigOperationResponse response = new ConfigOperationResponse(); + response.setSuccess(true); + return response; + } + + public static ConfigOperationResponse success(Object result) { + ConfigOperationResponse response = success(); + response.setResult(result); + return response; + } + + public static ConfigOperationResponse fail() { + ConfigOperationResponse response = new ConfigOperationResponse(); + response.setSuccess(false); + return response; + } + + public static ConfigOperationResponse fail(String errMsg) { + ConfigOperationResponse response = fail(); + response.setErrMsg(errMsg); + return response; + } +} diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/serializer/CustomDeserializer.java b/server/src/main/java/org/apache/seata/server/cluster/raft/serializer/CustomDeserializer.java index 533f5c3d678..7105ba74966 100644 --- a/server/src/main/java/org/apache/seata/server/cluster/raft/serializer/CustomDeserializer.java +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/serializer/CustomDeserializer.java @@ -17,6 +17,9 @@ package org.apache.seata.server.cluster.raft.serializer; import java.io.IOException; +import java.util.Arrays; +import java.util.List; + import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; @@ -31,6 +34,11 @@ public class CustomDeserializer extends JsonDeserializer> { String permitPackage = "org.apache.seata"; + List permitClass = Arrays.asList( + "java.util.Map", + "java.util.HashMap", + "java.util.LinkedHashMap" + ); @Override public Class deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { @@ -38,6 +46,13 @@ public Class deserialize(JsonParser jsonParser, DeserializationContext deseri if (className.startsWith(oldPackage)) { className = className.replaceFirst(oldPackage, currentPackage); } + if (permitClass.contains(className)) { + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e.getMessage(), e); + } + } if (className.startsWith(permitPackage)) { try { return Class.forName(className); diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/snapshot/RaftSnapshot.java b/server/src/main/java/org/apache/seata/server/cluster/raft/snapshot/RaftSnapshot.java index da97df7f5b9..cecc4c9f0a6 100644 --- a/server/src/main/java/org/apache/seata/server/cluster/raft/snapshot/RaftSnapshot.java +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/snapshot/RaftSnapshot.java @@ -137,8 +137,11 @@ public enum SnapshotType { /** * leader metadata snapshot */ - leader_metadata("leader_metadata"); - + leader_metadata("leader_metadata"), + /** + * config snapshot + */ + config("config"); final String type; SnapshotType(String type) { diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/snapshot/RaftSnapshotSerializer.java b/server/src/main/java/org/apache/seata/server/cluster/raft/snapshot/RaftSnapshotSerializer.java index 28aa70b9937..b8fef1970be 100644 --- a/server/src/main/java/org/apache/seata/server/cluster/raft/snapshot/RaftSnapshotSerializer.java +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/snapshot/RaftSnapshotSerializer.java @@ -50,6 +50,7 @@ public class RaftSnapshotSerializer { PERMITS.add(io.seata.server.cluster.raft.snapshot.RaftSnapshot.class.getName()); PERMITS.add(io.seata.server.cluster.raft.snapshot.RaftSnapshot.SnapshotType.class.getName()); PERMITS.add(java.lang.Enum.class.getName()); + PERMITS.add(java.util.HashMap.class.getName()); PERMITS.add("[B"); } diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/snapshot/config/ConfigSnapshotFile.java b/server/src/main/java/org/apache/seata/server/cluster/raft/snapshot/config/ConfigSnapshotFile.java new file mode 100644 index 00000000000..971902cb98d --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/snapshot/config/ConfigSnapshotFile.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.server.cluster.raft.snapshot.config; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.util.Map; +import java.util.Objects; + +import com.alipay.sofa.jraft.Status; +import com.alipay.sofa.jraft.error.RaftError; +import com.alipay.sofa.jraft.storage.snapshot.SnapshotReader; +import com.alipay.sofa.jraft.storage.snapshot.SnapshotWriter; +import org.apache.seata.common.loader.EnhancedServiceLoader; +import org.apache.seata.config.Configuration; +import org.apache.seata.config.ConfigurationFactory; +import org.apache.seata.config.store.ConfigStoreManager; +import org.apache.seata.config.store.ConfigStoreManagerProvider; +import org.apache.seata.config.store.rocksdb.RocksDBConfigStoreManager; +import org.apache.seata.server.cluster.raft.snapshot.RaftSnapshot; +import org.apache.seata.server.cluster.raft.snapshot.StoreSnapshotFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.seata.common.ConfigurationKeys.CONFIG_STORE_TYPE; +import static org.apache.seata.common.DefaultValues.DEFAULT_DB_TYPE; + +public class ConfigSnapshotFile implements Serializable, StoreSnapshotFile { + + private static final Logger LOGGER = LoggerFactory.getLogger(ConfigSnapshotFile.class); + + private static final long serialVersionUID = 1452307567830545914L; + + private static final Configuration FILE_CONFIG = ConfigurationFactory.CURRENT_FILE_INSTANCE; + + private static ConfigStoreManager configStoreManager; + private final String group; + + private final String fileName = "config"; + + public ConfigSnapshotFile(String group) { + this.group = group; + String dbType = FILE_CONFIG.getConfig(CONFIG_STORE_TYPE, DEFAULT_DB_TYPE); + configStoreManager = EnhancedServiceLoader.load(ConfigStoreManagerProvider.class, Objects.requireNonNull(dbType), false).provide(); + } + + @Override + public Status save(SnapshotWriter writer) { + Map> configMap = configStoreManager.getConfigMap(); + RaftSnapshot raftSnapshot = new RaftSnapshot(); + raftSnapshot.setBody(configMap); + raftSnapshot.setType(RaftSnapshot.SnapshotType.config); + LOGGER.info("groupId: {}, config size: {}", group, configMap.size()); + String path = new StringBuilder(writer.getPath()).append(File.separator).append(fileName).toString(); + try { + if (save(raftSnapshot, path)) { + if (writer.addFile(fileName)) { + return Status.OK(); + } else { + return new Status(RaftError.EIO, "Fail to add file to writer"); + } + } + } catch (IOException e) { + LOGGER.error("Fail to save groupId: {} snapshot {}", group, path, e); + } + return new Status(RaftError.EIO, "Fail to save groupId: " + group + " snapshot %s", path); + } + + @Override + public boolean load(SnapshotReader reader) { + if (reader.getFileMeta(fileName) == null) { + LOGGER.error("Fail to find data file in {}", reader.getPath()); + return false; + } + String path = new StringBuilder(reader.getPath()).append(File.separator).append(fileName).toString(); + try { + LOGGER.info("on snapshot load start index: {}", reader.load().getLastIncludedIndex()); + Map> configMap = (Map>)load(path); + ConfigStoreManager configStoreManager = RocksDBConfigStoreManager.getInstance(); + configStoreManager.clearData(); + configStoreManager.putConfigMap(configMap); + if (LOGGER.isInfoEnabled()) { + LOGGER.info("on snapshot load end index: {}", reader.load().getLastIncludedIndex()); + } + return true; + } catch (final Exception e) { + LOGGER.error("fail to load snapshot from {}", path, e); + return false; + } + } +} diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/snapshot/metadata/ConfigLeaderMetadataSnapshotFile.java b/server/src/main/java/org/apache/seata/server/cluster/raft/snapshot/metadata/ConfigLeaderMetadataSnapshotFile.java new file mode 100644 index 00000000000..8089d30ba9c --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/snapshot/metadata/ConfigLeaderMetadataSnapshotFile.java @@ -0,0 +1,88 @@ +/* + * 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.snapshot.metadata; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; + +import com.alipay.sofa.jraft.Status; +import com.alipay.sofa.jraft.error.RaftError; +import com.alipay.sofa.jraft.storage.snapshot.SnapshotReader; +import com.alipay.sofa.jraft.storage.snapshot.SnapshotWriter; +import org.apache.seata.server.cluster.raft.RaftConfigServerManager; +import org.apache.seata.server.cluster.raft.snapshot.RaftSnapshot; +import org.apache.seata.server.cluster.raft.snapshot.StoreSnapshotFile; +import org.apache.seata.server.cluster.raft.sync.msg.dto.RaftClusterMetadata; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ConfigLeaderMetadataSnapshotFile implements Serializable, StoreSnapshotFile { + private static final long serialVersionUID = 43235664615355354L; + + private static final Logger LOGGER = LoggerFactory.getLogger(ConfigLeaderMetadataSnapshotFile.class); + + private final String group; + + private final String fileName = "leader_metadata"; + + public ConfigLeaderMetadataSnapshotFile(String group) { + this.group = group; + } + + + @Override + public Status save(SnapshotWriter writer) { + RaftSnapshot raftSnapshot = new RaftSnapshot(); + RaftClusterMetadata raftClusterMetadata = + RaftConfigServerManager.getRaftServer().getRaftStateMachine().getRaftLeaderMetadata(); + raftSnapshot.setBody(raftClusterMetadata); + raftSnapshot.setType(RaftSnapshot.SnapshotType.leader_metadata); + String path = new StringBuilder(writer.getPath()).append(File.separator).append(fileName).toString(); + try { + if (save(raftSnapshot, path)) { + if (writer.addFile(fileName)) { + return Status.OK(); + } else { + return new Status(RaftError.EIO, "Fail to add file to writer"); + } + } + } catch (IOException e) { + LOGGER.error("Fail to save groupId: {} snapshot {}", group, path, e); + } + return new Status(RaftError.EIO, "Fail to save groupId: " + group + " snapshot %s", path); + } + + @Override + public boolean load(SnapshotReader reader) { + if (reader.getFileMeta(fileName) == null) { + LOGGER.error("Fail to find data file in {}", reader.getPath()); + return false; + } + String path = new StringBuilder(reader.getPath()).append(File.separator).append(fileName).toString(); + try { + RaftClusterMetadata raftClusterMetadata = (RaftClusterMetadata)load(path); + RaftConfigServerManager.getRaftServer().getRaftStateMachine() + .setRaftLeaderMetadata(raftClusterMetadata); + return true; + } catch (final Exception e) { + LOGGER.error("fail to load snapshot from {}", path, e); + return false; + } + } +} + diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/sync/msg/RaftConfigOperationSyncMsg.java b/server/src/main/java/org/apache/seata/server/cluster/raft/sync/msg/RaftConfigOperationSyncMsg.java new file mode 100644 index 00000000000..53a0c3cb281 --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/sync/msg/RaftConfigOperationSyncMsg.java @@ -0,0 +1,45 @@ +/* + * 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.sync.msg; + +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.server.cluster.raft.sync.msg.dto.ConfigOperationDTO; + + +public class RaftConfigOperationSyncMsg extends RaftBaseMsg { + + private static final long serialVersionUID = -3344345671349834321L; + private ConfigOperationDTO configOperation; + + public RaftConfigOperationSyncMsg(ConfigOperationDTO configOperation) { + this.msgType = RaftSyncMsgType.CONFIG_OPERATION; + this.configOperation = configOperation; + } + + public RaftConfigOperationSyncMsg() { + } + + public ConfigOperationDTO getConfigOperation() { + return configOperation; + } + + @Override + public String toString() { + return StringUtils.toString(this); + } + +} diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/sync/msg/RaftSyncMsgType.java b/server/src/main/java/org/apache/seata/server/cluster/raft/sync/msg/RaftSyncMsgType.java index fc9829d323b..ba030133438 100644 --- a/server/src/main/java/org/apache/seata/server/cluster/raft/sync/msg/RaftSyncMsgType.java +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/sync/msg/RaftSyncMsgType.java @@ -56,7 +56,10 @@ public enum RaftSyncMsgType { * refresh cluster metadata */ REFRESH_CLUSTER_METADATA, - + /** + * config operation + */ + CONFIG_OPERATION, /** * add vgroup mapping */ diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/sync/msg/closure/ConfigClosure.java b/server/src/main/java/org/apache/seata/server/cluster/raft/sync/msg/closure/ConfigClosure.java new file mode 100644 index 00000000000..ade72303882 --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/sync/msg/closure/ConfigClosure.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.server.cluster.raft.sync.msg.closure; + +import com.alipay.sofa.jraft.Closure; +import com.alipay.sofa.jraft.Status; +import org.apache.seata.server.cluster.raft.processor.response.ConfigOperationResponse; +import org.apache.seata.server.cluster.raft.sync.msg.RaftBaseMsg; + +/** + * The type of closure for configuration sync in raft + */ +public class ConfigClosure implements Closure { + + private RaftBaseMsg raftBaseMsg; + private ConfigOperationResponse response; + private Closure done; + + @Override + public void run(Status status) { + if (done != null) { + done.run(status); + } + } + + public RaftBaseMsg getRaftBaseMsg() { + return raftBaseMsg; + } + + public void setRaftBaseMsg(RaftBaseMsg raftBaseMsg) { + this.raftBaseMsg = raftBaseMsg; + } + + public ConfigOperationResponse getResponse() { + return response; + } + + public void setResponse(ConfigOperationResponse response) { + this.response = response; + } + + public Closure getDone() { + return done; + } + + public void setDone(Closure done) { + this.done = done; + } +} diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/sync/msg/dto/ConfigOperationDTO.java b/server/src/main/java/org/apache/seata/server/cluster/raft/sync/msg/dto/ConfigOperationDTO.java new file mode 100644 index 00000000000..0f3e0795f43 --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/sync/msg/dto/ConfigOperationDTO.java @@ -0,0 +1,98 @@ +/* + * 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.sync.msg.dto; + +import java.io.Serializable; + +import org.apache.seata.server.cluster.raft.execute.config.ConfigOperationType; +import org.apache.seata.server.cluster.raft.processor.request.ConfigOperationRequest; + +public class ConfigOperationDTO implements Serializable { + private static final long serialVersionUID = -1237293571963636954L; + + private ConfigOperationType optType; + private String namespace; + private String dataId; + private String key; + private Object value; + + public ConfigOperationDTO() { + } + + public ConfigOperationDTO(ConfigOperationType optType, String namespace, String dataId, String key, Object value) { + this.optType = optType; + this.namespace = namespace; + this.dataId = dataId; + this.key = key; + this.value = value; + } + + public ConfigOperationType getOptType() { + return optType; + } + + public void setOptType(ConfigOperationType optType) { + this.optType = optType; + } + + 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; + } + + 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 static ConfigOperationDTO convertConfigRequest2Dto(ConfigOperationRequest request) { + return new ConfigOperationDTO(request.getOptType(), request.getNamespace(), request.getDataId(), request.getKey(), request.getValue()); + } + + @Override + public String toString() { + return "ConfigOperationDTO{" + + "optType=" + optType + + ", namespace='" + namespace + '\'' + + ", dataId='" + dataId + '\'' + + ", key='" + key + '\'' + + ", value=" + value + + '}'; + } +} diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/util/RaftConfigTaskUtil.java b/server/src/main/java/org/apache/seata/server/cluster/raft/util/RaftConfigTaskUtil.java new file mode 100644 index 00000000000..2d50d6249eb --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/util/RaftConfigTaskUtil.java @@ -0,0 +1,78 @@ +/* + * 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.util; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import com.alipay.sofa.jraft.Closure; +import com.alipay.sofa.jraft.entity.Task; +import org.apache.seata.core.exception.GlobalTransactionException; +import org.apache.seata.core.exception.TransactionException; +import org.apache.seata.core.exception.TransactionExceptionCode; +import org.apache.seata.server.cluster.raft.RaftConfigServerManager; +import org.apache.seata.server.cluster.raft.sync.RaftSyncMessageSerializer; +import org.apache.seata.server.cluster.raft.sync.msg.RaftSyncMessage; + +/** + */ +public class RaftConfigTaskUtil { + public static boolean createTask(Closure done, Object data, CompletableFuture completableFuture) + throws TransactionException { + final Task task = new Task(); + if (data != null) { + RaftSyncMessage raftSyncMessage = new RaftSyncMessage(); + raftSyncMessage.setBody(data); + try { + task.setData(ByteBuffer.wrap(RaftSyncMessageSerializer.encode(raftSyncMessage))); + } catch (IOException e) { + throw new TransactionException(e); + } + } + task.setDone(done == null ? status -> { + } : done); + RaftConfigServerManager.getRaftServer().getNode().apply(task); + if (completableFuture != null) { + return futureGet(completableFuture); + } + return true; + } + + public static boolean createTask(Closure done, CompletableFuture completableFuture) + throws TransactionException { + return createTask(done, null, completableFuture); + } + + public static boolean futureGet(CompletableFuture completableFuture) throws TransactionException { + try { + return completableFuture.get(); + } catch (InterruptedException e) { + throw new GlobalTransactionException(TransactionExceptionCode.FailedWriteSession, + "Fail to store global session: " + e.getMessage()); + } catch (ExecutionException e) { + if (e.getCause() instanceof TransactionException) { + throw (TransactionException)e.getCause(); + } else { + throw new GlobalTransactionException(TransactionExceptionCode.FailedWriteSession, + "Fail to store global session: " + e.getMessage()); + } + } + } + +} diff --git a/server/src/main/java/org/apache/seata/server/cluster/watch/ConfigWatcher.java b/server/src/main/java/org/apache/seata/server/cluster/watch/ConfigWatcher.java new file mode 100644 index 00000000000..d2dfcd6c725 --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/cluster/watch/ConfigWatcher.java @@ -0,0 +1,89 @@ +/* + * 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.watch; + +import static org.apache.seata.server.cluster.watch.Watcher.Protocol.HTTP; + +public class ConfigWatcher { + private String namespace; + + private String dataId; + + private volatile boolean done = false; + + private T asyncContext; + + private long timeout; + + + private String protocol = HTTP; + + public ConfigWatcher(String namespace, String dataId, T asyncContext, int timeout) { + this.namespace = namespace; + this.dataId = dataId; + this.asyncContext = asyncContext; + this.timeout = System.currentTimeMillis() + timeout; + } + + 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; + } + + public boolean isDone() { + return done; + } + + public void setDone(boolean done) { + this.done = done; + } + + public T getAsyncContext() { + return asyncContext; + } + + public void setAsyncContext(T asyncContext) { + this.asyncContext = asyncContext; + } + + public long getTimeout() { + return timeout; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } +} diff --git a/server/src/main/java/org/apache/seata/server/config/ConfigurationProcessor.java b/server/src/main/java/org/apache/seata/server/config/ConfigurationProcessor.java new file mode 100644 index 00000000000..06e6d6d7487 --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/config/ConfigurationProcessor.java @@ -0,0 +1,108 @@ +/* + * 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.config; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.seata.common.Constants; +import org.apache.seata.config.dto.ConfigurationItem; +import org.apache.seata.config.dto.ConfigurationItemMeta; +import org.apache.seata.config.store.rocksdb.RocksDBConfigStoreManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.yaml.snakeyaml.Yaml; + + +/** + * The configuration items processor + * + */ +public class ConfigurationProcessor { + private static final Logger LOGGER = LoggerFactory.getLogger(RocksDBConfigStoreManager.class); + private static final String ENCRYPT_STRING = "******"; + private static final String NOT_SEATA_CONFIG = "Not Seata configuration"; + private static final Map CONFIGURATION_ITEMS_META_MAP = new HashMap<>(); + private static final String META_FILE_NAME = Constants.CONFIGURATION_META_FILE_NAME; + private static final String CONFIG_META_KEY = "configuration-meta"; + private static final String META_KEY_KEY = "key"; + private static final String META_DESC_KEY = "desc"; + private static final String META_DEFAULT_VALUE_KEY = "defaultValue"; + private static final String META_ENCRYPT_KEY = "isEncrypt"; + + static { + loadConfigurationItemMeta(); + } + + /** + * load Configuration items meta from local yaml file. + */ + @SuppressWarnings("unchecked") + private static void loadConfigurationItemMeta() { + try (InputStream inputStream = new ClassPathResource(META_FILE_NAME).getInputStream()) { + Yaml yaml = new Yaml(); + Map map = yaml.load(inputStream); + List> configItemMetaList = (List>) map.get(CONFIG_META_KEY); + for (Map metaMap : configItemMetaList) { + ConfigurationItemMeta itemMeta = mapToConfigItemMeta(metaMap); + CONFIGURATION_ITEMS_META_MAP.put(itemMeta.getKey(), itemMeta); + } + } catch (Exception e) { + LOGGER.error("Failed to load configuration meta file", e); + } + } + + private static ConfigurationItemMeta mapToConfigItemMeta(Map configItemMap) { + String key = (String) configItemMap.get(META_KEY_KEY); + String desc = (String) configItemMap.get(META_DESC_KEY); + Object defaultValue = configItemMap.get(META_DEFAULT_VALUE_KEY); + Boolean isEncrypt = (Boolean) configItemMap.get(META_ENCRYPT_KEY); + return new ConfigurationItemMeta(key, desc, defaultValue, isEncrypt); + } + /** + * process configuration items map (fill description and default value ,or encrypt sensitive data). + */ + public static Map processConfigMap(Map configMap) { + return configMap.entrySet().stream() + .map(entry -> { + String key = entry.getKey(); + Object value = entry.getValue(); + return new HashMap.SimpleEntry<>(key, processConfigItem(key, value)); + }) + .collect(HashMap::new, (map, entry) -> map.put(entry.getKey(), entry.getValue()), HashMap::putAll); + } + + /** + * Process ConfigurationItem + */ + public static ConfigurationItem processConfigItem(String key, Object value) { + ConfigurationItemMeta meta = CONFIGURATION_ITEMS_META_MAP.get(key); + ConfigurationItem item = new ConfigurationItem(); + item.setKey(key); + item.setDescription(meta == null ? NOT_SEATA_CONFIG : meta.getDescription()); + item.setDefaultValue(meta == null ? null : meta.getDefaultValue()); + if (meta != null && meta.getEncrypt()) { + item.setValue(ENCRYPT_STRING); + } else { + item.setValue(value); + } + return item; + } +} diff --git a/server/src/main/java/org/apache/seata/server/controller/ClusterController.java b/server/src/main/java/org/apache/seata/server/controller/ClusterController.java index 816f6e3e250..888e9d667e7 100644 --- a/server/src/main/java/org/apache/seata/server/controller/ClusterController.java +++ b/server/src/main/java/org/apache/seata/server/controller/ClusterController.java @@ -16,40 +16,66 @@ */ package org.apache.seata.server.controller; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Properties; import java.util.Set; + import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.servlet.AsyncContext; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; 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.rpc.InvokeContext; +import com.alipay.sofa.jraft.rpc.impl.cli.CliClientServiceImpl; import org.apache.seata.common.ConfigurationKeys; import org.apache.seata.common.metadata.MetadataResponse; import org.apache.seata.common.metadata.Node; import org.apache.seata.common.result.Result; import org.apache.seata.common.util.StringUtils; +import org.apache.seata.config.ConfigType; import org.apache.seata.config.ConfigurationFactory; +import org.apache.seata.config.processor.ConfigProcessor; +import org.apache.seata.config.store.ConfigStoreManager; +import org.apache.seata.config.store.ConfigStoreManagerFactory; +import org.apache.seata.core.serializer.SerializerType; +import org.apache.seata.server.cluster.manager.ClusterConfigWatcherManager; import org.apache.seata.server.cluster.manager.ClusterWatcherManager; +import org.apache.seata.server.cluster.raft.RaftConfigServer; +import org.apache.seata.server.cluster.raft.RaftConfigServerManager; import org.apache.seata.server.cluster.raft.RaftServer; import org.apache.seata.server.cluster.raft.RaftServerManager; +import org.apache.seata.server.cluster.raft.processor.request.ConfigOperationRequest; +import org.apache.seata.server.cluster.raft.processor.response.ConfigOperationResponse; import org.apache.seata.server.cluster.raft.sync.msg.dto.RaftClusterMetadata; +import org.apache.seata.server.cluster.watch.ConfigWatcher; import org.apache.seata.server.cluster.watch.Watcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.context.ApplicationContext; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import static org.apache.seata.common.ConfigurationKeys.SEATA_FILE_PREFIX_ROOT_CONFIG; import static org.apache.seata.common.ConfigurationKeys.STORE_MODE; +import static org.apache.seata.common.Constants.RAFT_CONFIG_GROUP; import static org.apache.seata.common.DefaultValues.DEFAULT_SEATA_GROUP; /** @@ -63,14 +89,33 @@ public class ClusterController { @Resource private ClusterWatcherManager clusterWatcherManager; + @Resource + private ClusterConfigWatcherManager clusterConfigWatcherManager; + private ServerProperties serverProperties; + private ConfigStoreManager configStoreManager; @Resource ApplicationContext applicationContext; + private static final LinkedHashMap SUFFIX_MAP = new LinkedHashMap(8) { + { + put("txt", "properties"); + put("text", "properties"); + put("properties", "properties"); + put("yml", "yaml"); + put("yaml", "yaml"); + } + }; @PostConstruct private void init() { this.serverProperties = applicationContext.getBean(ServerProperties.class); + // only initialize configStoreManager in raft configuration. + String configType = ConfigurationFactory.CURRENT_FILE_INSTANCE.getConfig(ConfigurationKeys.FILE_ROOT_CONFIG + + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR + ConfigurationKeys.FILE_ROOT_TYPE); + if (ConfigType.Raft.name().equalsIgnoreCase(configType)) { + configStoreManager = ConfigStoreManagerFactory.getInstance(); + } } @PostMapping("/changeCluster") @@ -89,6 +134,21 @@ public Result changeCluster(@RequestParam String raftClusterStr) { return result; } + @PostMapping("/changeConfigCluster") + public Result changeConfigCluster(@RequestParam String raftClusterStr) { + Result result = new Result<>(); + final Configuration newConf = new Configuration(); + if (!newConf.parse(raftClusterStr)) { + result.setMessage("fail to parse initConf:" + raftClusterStr); + } else { + String group = RaftConfigServerManager.getGroup(); + RaftConfigServerManager.getCliServiceInstance().changePeers(group, + RouteTable.getInstance().getConfiguration(group), newConf); + RouteTable.getInstance().updateConfiguration(group, newConf); + } + return result; + } + @GetMapping("/cluster") public MetadataResponse cluster(String group) { MetadataResponse metadataResponse = new MetadataResponse(); @@ -122,6 +182,187 @@ public MetadataResponse cluster(String group) { return metadataResponse; } + @GetMapping("/config/cluster") + public MetadataResponse configCluster() { + MetadataResponse metadataResponse = new MetadataResponse(); + RaftConfigServer raftServer = RaftConfigServerManager.getRaftServer(); + if (raftServer != null) { + String configType = ConfigurationFactory.CURRENT_FILE_INSTANCE.getConfig(ConfigurationKeys.FILE_ROOT_CONFIG + + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR + ConfigurationKeys.FILE_ROOT_TYPE); + metadataResponse.setConfigMode(configType); + RouteTable routeTable = RouteTable.getInstance(); + try { + routeTable.refreshLeader(RaftConfigServerManager.getCliClientServiceInstance(), RAFT_CONFIG_GROUP , 1000); + PeerId leader = routeTable.selectLeader(RAFT_CONFIG_GROUP); + if (leader != null) { + Set nodes = new HashSet<>(); + RaftClusterMetadata raftClusterMetadata = raftServer.getRaftStateMachine().getRaftLeaderMetadata(); + Node leaderNode = raftServer.getRaftStateMachine().getRaftLeaderMetadata().getLeader(); + leaderNode.setGroup(RAFT_CONFIG_GROUP); + nodes.add(leaderNode); + nodes.addAll(raftClusterMetadata.getLearner()); + nodes.addAll(raftClusterMetadata.getFollowers()); + metadataResponse.setTerm(raftClusterMetadata.getTerm()); + metadataResponse.setNodes(new ArrayList<>(nodes)); + } + } catch (Exception e) { + LOGGER.error("there is an exception to getting the leader address: {}", e.getMessage(), e); + } + } + return metadataResponse; + } + + @GetMapping("/config/get") + public ConfigOperationResponse getConfig(String namespace, String dataId, String key) { + try { + checkParam(namespace, "namespace"); + checkParam(dataId, "dataId"); + checkParam(key, "key"); + } catch (IllegalArgumentException e) { + return ConfigOperationResponse.fail(e.getMessage()); + } + ConfigOperationRequest request = ConfigOperationRequest.buildGetRequest(namespace, dataId, key); + return executeConfigOperationRequest(request); + } + + @PostMapping("/config/put") + public ConfigOperationResponse putConfig(String namespace, String dataId, String key, String value) { + try { + checkParam(namespace, "namespace"); + checkParam(dataId, "dataId"); + checkParam(key, "key"); + checkParam(value, "value"); + } catch (IllegalArgumentException e) { + return ConfigOperationResponse.fail(e.getMessage()); + } + ConfigOperationRequest request = ConfigOperationRequest.buildPutRequest(namespace, dataId, key, value); + return executeConfigOperationRequest(request); + } + + @DeleteMapping("/config/delete") + public ConfigOperationResponse deleteConfig(String namespace, String dataId, String key) { + try { + checkParam(namespace, "namespace"); + checkParam(dataId, "dataId"); + checkParam(key, "key"); + } catch (IllegalArgumentException e) { + return ConfigOperationResponse.fail(e.getMessage()); + } + ConfigOperationRequest request = ConfigOperationRequest.buildDeleteRequest(namespace, dataId, key); + return executeConfigOperationRequest(request); + } + + @DeleteMapping("/config/deleteAll") + public ConfigOperationResponse deleteAllConfig(String namespace, String dataId) { + try { + checkParam(namespace, "namespace"); + checkParam(dataId, "dataId"); + } catch (IllegalArgumentException e) { + return ConfigOperationResponse.fail(e.getMessage()); + } + ConfigOperationRequest request = ConfigOperationRequest.buildDeleteAllRequest(namespace, dataId); + return executeConfigOperationRequest(request); + } + + @GetMapping("/config/getAll") + public ConfigOperationResponse getAllConfig(String namespace, String dataId) { + try { + checkParam(namespace, "namespace"); + checkParam(dataId, "dataId"); + } catch (IllegalArgumentException e) { + return ConfigOperationResponse.fail(e.getMessage()); + } + ConfigOperationRequest request = ConfigOperationRequest.buildGetAllRequest(namespace, dataId); + return executeConfigOperationRequest(request); + } + + @PostMapping("/config/upload") + public ConfigOperationResponse uploadConfig(@RequestParam("namespace") String namespace, @RequestParam("dataId") String dataId, @RequestParam("file") MultipartFile file) { + try { + checkParam(namespace, "namespace"); + checkParam(dataId, "dataId"); + } catch (IllegalArgumentException e) { + return ConfigOperationResponse.fail(e.getMessage()); + } + if (file == null || file.isEmpty()) { + return ConfigOperationResponse.fail("The configuration file cannot be empty!"); + } + String fileName = file.getOriginalFilename(); + String dataType = SUFFIX_MAP.get(getFileType(fileName)); + if (StringUtils.isEmpty(dataType)) { + return ConfigOperationResponse.fail("The configuration file type is not supported!"); + } + StringBuilder sb = new StringBuilder(); + Map configMap = new HashMap<>(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append('\n'); + } + Properties properties = ConfigProcessor.processConfig(sb.toString(), dataType); + for (String key : properties.stringPropertyNames()) { + String value = properties.getProperty(key); + // remove 'seata.' prefix compatible with the config under Spring Boot + if (key.startsWith(SEATA_FILE_PREFIX_ROOT_CONFIG)) { + key = key.substring(SEATA_FILE_PREFIX_ROOT_CONFIG.length()); + } + configMap.put(key, value); + } + } catch (IOException e) { + LOGGER.error("Failed to read config file: {}", e.getMessage()); + return ConfigOperationResponse.fail("Failed to read config file"); + } + ConfigOperationRequest request = ConfigOperationRequest.buildUploadRequest(namespace, dataId, configMap); + return executeConfigOperationRequest(request); + } + + private static String getFileType(String fileName) { + if (StringUtils.isEmpty(fileName)) { + return null; + } + return fileName.substring(fileName.lastIndexOf(".") + 1); + } + + @GetMapping("/config/getNamespaces") + public ConfigOperationResponse getNamespaces() { + ConfigOperationRequest request = ConfigOperationRequest.buildGetNamespaces(); + return executeConfigOperationRequest(request); + } + + @GetMapping("/config/getDataIds") + public ConfigOperationResponse getDataIds(String namespace) { + try { + checkParam(namespace, "namespace"); + } catch (IllegalArgumentException e) { + return ConfigOperationResponse.fail(e.getMessage()); + } + ConfigOperationRequest request = ConfigOperationRequest.buildGetDataIds(namespace); + return executeConfigOperationRequest(request); + } + + private ConfigOperationResponse executeConfigOperationRequest(ConfigOperationRequest request) { + PeerId leader = RaftConfigServerManager.getLeader(); + if (leader == null) { + return ConfigOperationResponse.fail("failed to get leader"); + } + InvokeContext invokeContext = new InvokeContext(); + invokeContext.put(com.alipay.remoting.InvokeContext.BOLT_CUSTOM_SERIALIZER, + SerializerType.JACKSON.getCode()); + CliClientServiceImpl cliClientService = (CliClientServiceImpl)RaftConfigServerManager.getCliClientServiceInstance(); + try { + return (ConfigOperationResponse)cliClientService.getRpcClient().invokeSync(leader.getEndpoint(), request, invokeContext, 1000); + } catch (Exception e) { + LOGGER.error("Failed to execute request: {}", request.toString()); + return ConfigOperationResponse.fail(e.getMessage()); + } + } + + private void checkParam(final String param, final String key) { + if (StringUtils.isEmpty(param)) { + throw new IllegalArgumentException("Param '" + key + "' is required."); + } + } + @PostMapping("/watch") public void watch(HttpServletRequest request, @RequestParam Map groupTerms, @RequestParam(defaultValue = "28000") int timeout) { @@ -134,4 +375,22 @@ public void watch(HttpServletRequest request, @RequestParam Map }); } + @PostMapping("/config/watch") + public void configWatch(HttpServletRequest request, @RequestParam String namespace, @RequestParam String dataId, @RequestParam(required = false) Long version, + @RequestParam(defaultValue = "28000") int timeout) { + Long currentVersion = configStoreManager.getConfigVersion(namespace, dataId); + // if the config version of client is lower than the server, return directly + if (version == null || (currentVersion != null && version < currentVersion)) { + AsyncContext context = request.startAsync(); + HttpServletResponse httpServletResponse = (HttpServletResponse) context.getResponse(); + httpServletResponse.setStatus(HttpServletResponse.SC_OK); + context.complete(); + return; + } + AsyncContext context = request.startAsync(); + context.setTimeout(0L); + ConfigWatcher configWatcher = new ConfigWatcher<>(namespace, dataId, context, timeout); + clusterConfigWatcherManager.registryWatcher(configWatcher); + } + } diff --git a/server/src/main/resources/META-INF/spring.factories b/server/src/main/resources/META-INF/spring.factories index 649fa8a356c..4b25a436d41 100644 --- a/server/src/main/resources/META-INF/spring.factories +++ b/server/src/main/resources/META-INF/spring.factories @@ -15,4 +15,7 @@ # limitations under the License. # org.springframework.context.ApplicationListener=\ -org.apache.seata.server.spring.listener.ServerApplicationListener \ No newline at end of file +org.apache.seata.server.spring.listener.ServerApplicationListener + +org.springframework.context.ApplicationContextInitializer=\ +org.apache.seata.server.cluster.raft.RaftConfigInitializer \ No newline at end of file diff --git a/server/src/main/resources/configuration-meta.yml b/server/src/main/resources/configuration-meta.yml new file mode 100644 index 00000000000..c3cbe593e77 --- /dev/null +++ b/server/src/main/resources/configuration-meta.yml @@ -0,0 +1,625 @@ +# +# 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. +# + +configuration-meta: + # common configuration + - key: 'transport.type' + desc: 'socket 通信方式' + defaultValue: TCP + isEncrypt: false + + - key: 'transport.server' + desc: 'socket 通道类型' + defaultValue: null + isEncrypt: false + + - key: 'transport.threadFactory.bossThreadSize' + desc: 'Netty 通信模型 Boss group 线程数' + defaultValue: 1 + isEncrypt: false + + - key: 'transport.threadFactory.workerThreadSize' + desc: 'Netty 通信模型 Worker group 线程数,线程的默认工作模式有 4 种: Auto(2*CPU 核数 + 1)、Pin(CPU 核数)、BusyPin(CPU 核数 + 1)、Default(2*CPU 核数)' + defaultValue: Default + isEncrypt: false + + - key: 'transport.shutdown.wait' + desc: '服务端 Netty 线程池关闭前等待服务下线时间' + defaultValue: 3 + isEncrypt: false + + - key: 'transport.serialization' + desc: 'client 和 server 通信编解码方式' + defaultValue: seata + isEncrypt: false + + - key: 'transport.compressor' + desc: 'client 和 server 通信数据压缩方式,支持 none、gzip、zip、sevenz、bzip2、lz4、deflater、zstd' + defaultValue: none + isEncrypt: false + + - key: 'transport.heartbeat' + desc: 'client 和 server 通信心跳检测开关' + defaultValue: true + isEncrypt: false + + - key: 'registry.type' + desc: '注册中心类型,支持 file 、nacos 、redis、eureka、zk、consul、etcd3、sofa、custom' + defaultValue: file + isEncrypt: false + + - key: 'config.type' + desc: '配置中心类型,支持 file、nacos 、apollo、zk、consul、etcd3、springcloud、custom' + defaultValue: file + isEncrypt: false + + # server configuration + - key: 'transport.enableTcServerBatchSendResponse' + desc: 'TC 批量发送回复消息开关' + defaultValue: false + isEncrypt: false + + - key: 'transport.rpcTcRequestTimeout' + desc: 'TC 二阶段下发请求超时时间' + defaultValue: 30 + isEncrypt: false + + - key: 'server.undo.logSaveDays' + desc: 'undo 保留天数,log_status=1 和未正常清理的 undo' + defaultValue: 7 + isEncrypt: false + + - key: 'server.undo.logDeletePeriod' + desc: 'undo 清理线程间隔时间' + defaultValue: 86400000 + isEncrypt: false + + - key: 'server.maxCommitRetryTimeout' + desc: '二阶段提交重试超时时长,单位 ms,s,m,h,d,默认值为-1 表示无限重试' + defaultValue: -1 + isEncrypt: false + + - key: 'server.maxRollbackRetryTimeout' + desc: '二阶段回滚重试超时时长,同 commit' + defaultValue: -1 + isEncrypt: false + + - key: 'server.recovery.committingRetryPeriod' + desc: '二阶段提交未完成状态全局事务重试提交线程间隔时间' + defaultValue: 1000 + isEncrypt: false + + - key: 'server.recovery.asynCommittingRetryPeriod' + desc: '二阶段异步提交状态重试提交线程间隔时间' + defaultValue: 1000 + isEncrypt: false + + - key: 'server.recovery.rollbackingRetryPeriod' + desc: '二阶段回滚状态重试回滚线程间隔时间' + defaultValue: 1000 + isEncrypt: false + + - key: 'server.recovery.timeoutRetryPeriod' + desc: '超时状态检测重试线程间隔时间,检测出超时将全局事务置入回滚会话管理器' + defaultValue: 1000 + isEncrypt: false + + - key: 'server.rollbackRetryTimeoutUnlockEnable' + desc: '二阶段回滚超时后是否释放锁' + defaultValue: false + isEncrypt: false + + - key: 'server.distributedLockExpireTime' + desc: 'Server 端事务管理全局锁超时时间' + defaultValue: 10000 + isEncrypt: false + + - key: 'server.server.xaerNotaRetryTimeout' + desc: '防止 XA 分支事务悬挂的重试超时时间' + defaultValue: 60000 + isEncrypt: false + + - key: 'server.session.branchAsyncQueueSize' + desc: '分支事务 Session 异步删除线程池队列大小' + defaultValue: 5000 + isEncrypt: false + + - key: 'server.session.enableBranchAsyncRemove' + desc: '分支事务 Session 异步删除开关' + defaultValue: false + isEncrypt: false + + - key: 'server.enableParallelRequestHandle' + desc: '对于批量请求消息的并行处理开关' + defaultValue: true + isEncrypt: false + + - key: 'server.enableParallelHandleBranch' + desc: '二阶段并行下发开关' + defaultValue: false + isEncrypt: false + + - key: 'server.applicationDataLimitCheck' + desc: '是否开启应用数据大小检查' + defaultValue: false + isEncrypt: false + + - key: 'server.applicationDataLimit' + desc: '应用数据大小限制' + defaultValue: 64000 + isEncrypt: false + + - key: 'server.raft.group' + desc: 'raft 存储模式下的 group,client 的事务分组值要与之对应' + defaultValue: default + isEncrypt: false + + - key: 'server.raft.server-addr' + desc: 'raft 集群列表如 192.168.0.111:9091,192.168.0.112:9091' + defaultValue: null + isEncrypt: false + + - key: 'server.raft.snapshot-interval' + desc: '间隔多久做一次内存快照,暂停状态机,但能提高停机恢复速度' + defaultValue: 600 + isEncrypt: false + + - key: 'server.raft.apply-batch' + desc: '任务累积批次后提交至 leader' + defaultValue: 32 + isEncrypt: false + + - key: 'server.raft.max-append-bufferSize' + desc: 'raft 日志存储缓冲区最大大小' + defaultValue: 256K + isEncrypt: false + + - key: 'server.raft.max-replicator-inflight-msgs' + desc: '启用 pipeline 请求情况下,最大 in-flight 请求数' + defaultValue: 256 + isEncrypt: false + + - key: 'server.raft.disruptor-buffer-size' + desc: '内部 disruptor buffer 大小,适当调高该值适应写入吞吐量高场景' + defaultValue: 16384 + isEncrypt: false + + - key: 'server.raft.election-timeout-ms' + desc: '超过多久没有 leader 心跳开始重选举' + defaultValue: 1000 + isEncrypt: false + + - key: 'server.raft.reporter-enabled' + desc: 'raft 自身的监控是否开启' + defaultValue: false + isEncrypt: false + + - key: 'server.raft.reporter-initial-delay' + desc: '监控输出间隔' + defaultValue: 60 + isEncrypt: false + + - key: 'server.raft.serialization' + desc: '序列化方式,仅支持 jackson' + defaultValue: jackson + isEncrypt: false + + - key: 'server.raft.compressor' + desc: 'raftlog 和 snapshot 的压缩方式,支持 gzip, zstd, lz4' + defaultValue: none + isEncrypt: false + + - key: 'server.raft.sync' + desc: 'raftlog 同步刷盘' + defaultValue: true + isEncrypt: false + + - key: 'store.mode' + desc: '事务会话信息存储方式' + defaultValue: file + isEncrypt: false + + - key: 'store.lock.mode' + desc: '事务锁信息存储方式' + defaultValue: file + isEncrypt: false + + - key: 'store.session.mode' + desc: '事务回话信息存储方式' + defaultValue: file + isEncrypt: false + + - key: 'store.publicKey' + desc: 'db 或 redis 存储密码解密公钥' + defaultValue: null + isEncrypt: true + + - key: 'store.file.dir' + desc: 'file 模式文件存储文件夹名' + defaultValue: sessionStore + isEncrypt: false + + - key: 'store.file.maxBranchSessionSize' + desc: 'file 模式文件存储分支 session 最大字节数' + defaultValue: 16384(16kb) + isEncrypt: false + + - key: 'store.file.maxGlobalSessionSize' + desc: 'file 模式文件存储全局 session 最大字节数' + defaultValue: 512b + isEncrypt: false + + - key: 'store.file.fileWriteBufferCacheSize' + desc: 'file 模式文件存储 buffer 最大缓存大小' + defaultValue: 16384(16kb) + isEncrypt: false + + - key: 'store.file.flushDiskMode' + desc: 'file 模式文件存储刷盘策略' + defaultValue: async + isEncrypt: false + + - key: 'store.file.sessionReloadReadSize' + desc: 'file 模式文件存储 Server 节点重启后从备份文件中恢复的 session 或 lock key 上限个数' + defaultValue: 100 + isEncrypt: false + + - key: 'store.db.datasource' + desc: 'db 模式数据源类型' + defaultValue: '' + isEncrypt: false + + - key: 'store.db.dbType' + desc: 'db 模式数据库类型' + defaultValue: '' + isEncrypt: false + + - key: 'store.db.driverClassName' + desc: 'db 模式数据库驱动' + defaultValue: '' + isEncrypt: false + + - key: 'store.db.url' + desc: 'db 模式数据库 url' + defaultValue: '' + isEncrypt: false + + - key: 'store.db.user' + desc: 'db 模式数据库账户' + defaultValue: '' + isEncrypt: false + + - key: 'store.db.password' + desc: 'db 模式数据库账户密码' + defaultValue: '' + isEncrypt: true + + - key: 'store.db.minConn' + desc: 'db 模式数据库初始连接数' + defaultValue: 1 + isEncrypt: false + + - key: 'store.db.maxConn' + desc: 'db 模式数据库最大连接数' + defaultValue: 20 + isEncrypt: false + + - key: 'store.db.maxWait' + desc: 'db 模式获取连接时最大等待时间' + defaultValue: 5000 + isEncrypt: false + + - key: 'store.db.globalTable' + desc: 'db 模式全局事务表名' + defaultValue: 'global_table' + isEncrypt: false + + - key: 'store.db.branchTable' + desc: 'db 模式分支事务表名' + defaultValue: 'branch_table' + isEncrypt: false + + - key: 'store.db.lockTable' + desc: 'db 模式全局锁表名' + defaultValue: 'lock_table' + isEncrypt: false + + - key: 'store.db.queryLimit' + desc: 'db 模式查询全局事务一次的最大条数' + defaultValue: 100 + isEncrypt: false + + - key: 'store.db.distributedLockTable' + desc: 'db 模式 Sever 端事务管理全局锁存储表名' + defaultValue: 'distributed_lock' + isEncrypt: false + + - key: 'store.redis.mode' + desc: 'redis 模式' + defaultValue: single + isEncrypt: false + + - key: 'store.redis.single.host' + desc: '单机模式下 redis 的 host' + defaultValue: '' + isEncrypt: false + + - key: 'store.redis.single.port' + desc: '单机模式下 redis 的 port' + defaultValue: '' + isEncrypt: false + + - key: 'store.redis.sentinel.masterName' + desc: 'sentinel 模式下 redis 的主库名称' + defaultValue: '' + isEncrypt: false + + - key: 'store.redis.sentinel.sentinelHosts' + desc: 'sentinel 模式下 sentinel 的 hosts' + defaultValue: '' + isEncrypt: false + + - key: 'store.redis.host' + desc: 'redis 模式 ip' + defaultValue: '127.0.0.1' + isEncrypt: false + + - key: 'store.redis.port' + desc: 'redis 模式端口' + defaultValue: 6379 + isEncrypt: false + + - key: 'store.redis.maxConn' + desc: 'redis 模式最大连接数' + defaultValue: 10 + isEncrypt: false + + - key: 'store.redis.minConn' + desc: 'redis 模式最小连接数' + defaultValue: 1 + isEncrypt: false + + - key: 'store.redis.database' + desc: 'redis 模式默认库' + defaultValue: 0 + isEncrypt: false + + - key: 'store.redis.password' + desc: 'redis 模式密码' + defaultValue: 'null' + isEncrypt: true + + - key: 'store.redis.queryLimit' + desc: 'redis 模式一次查询最大条数' + defaultValue: 100 + isEncrypt: false + + - key: 'store.redis.type' + desc: 'redis 模式主要使用的方式: lua, pippline' + defaultValue: 'pippline' + isEncrypt: false + + - key: 'metrics.enabled' + desc: '是否启用 Metrics' + defaultValue: false + isEncrypt: false + + - key: 'metrics.registryType' + desc: '指标注册器类型' + defaultValue: 'compact' + isEncrypt: false + + - key: 'metrics.exporterList' + desc: '指标结果 Measurement 数据输出器列表' + defaultValue: 'prometheus' + isEncrypt: false + + - key: 'metrics.exporterPrometheusPort' + desc: 'prometheus 输出器 Client 端口号' + defaultValue: 9898 + isEncrypt: false + + # client configuration + - key: 'seata.enabled' + desc: '是否开启 spring-boot 自动装配' + defaultValue: true + isEncrypt: false + + - key: 'seata.enableAutoDataSourceProxy' + desc: '是否开启数据源自动代理' + defaultValue: true + isEncrypt: false + + - key: 'seata.useJdkProxy' + desc: '是否使用 JDK 代理作为数据源自动代理的实现方式' + defaultValue: false + isEncrypt: false + + - key: 'transport.enableClientBatchSendRequest' + desc: '客户端事务消息请求是否批量合并发送' + defaultValue: true + isEncrypt: false + + - key: 'transport.enableTmClientChannelCheckFailFast' + desc: '客户端 TM 快速失败检查' + defaultValue: true + isEncrypt: false + + - key: 'transport.enableRmClientChannelCheckFailFast' + desc: '客户端 RM 快速失败检查' + defaultValue: true + isEncrypt: false + + - key: 'client.log.exceptionRate' + desc: '日志异常输出概率' + defaultValue: 100 + isEncrypt: false + + - key: 'service.vgroupMapping.my_test_tx_group' + desc: '事务群组(附录 1)' + defaultValue: '' + isEncrypt: false + + - key: 'service.default.grouplist' + desc: 'TC 服务列表(附录 2)' + defaultValue: '' + isEncrypt: false + + - key: 'service.disableGlobalTransaction' + desc: '全局事务开关' + defaultValue: false + isEncrypt: false + + - key: 'client.tm.degradeCheck' + desc: '降级开关' + defaultValue: false + isEncrypt: false + + - key: 'client.tm.degradeCheckAllowTimes' + desc: '升降级达标阈值' + defaultValue: 10 + isEncrypt: false + + - key: 'client.tm.degradeCheckPeriod' + desc: '服务自检周期,单位 ms,每 2 秒进行一次服务自检' + defaultValue: 2000 + isEncrypt: false + + - key: 'client.rm.reportSuccessEnable' + desc: '是否上报一阶段成功' + defaultValue: false + isEncrypt: false + + - key: 'client.rm.asyncCommitBufferLimit' + desc: '异步提交缓存队列长度' + defaultValue: 10000 + isEncrypt: false + + - key: 'client.rm.lock.retryInterval' + desc: '校验或占用全局锁重试间隔,单位毫秒' + defaultValue: 10 + isEncrypt: false + + - key: 'client.rm.lock.retryTimes' + desc: '校验或占用全局锁重试次数' + defaultValue: 30 + isEncrypt: false + + - key: 'client.rm.lock.retryPolicyBranchRollbackOnConflict' + desc: '分支事务与其它全局回滚事务冲突时锁策略' + defaultValue: true + isEncrypt: false + + - key: 'client.rm.reportRetryCount' + desc: '一阶段结果上报 TC 重试次数' + defaultValue: 5 + isEncrypt: false + + - key: 'client.rm.tableMetaCheckEnable' + desc: '自动刷新缓存中的表结构' + defaultValue: false + isEncrypt: false + + - key: 'client.rm.tableMetaCheckerInterval' + desc: '定时刷新缓存中表结构间隔时间,单位秒' + defaultValue: 60 + isEncrypt: false + + - key: 'client.rm.sagaBranchRegisterEnable' + desc: '是否开启 saga 分支注册' + defaultValue: false + isEncrypt: false + + - key: 'client.rm.sagaJsonParser' + desc: 'saga 模式中数据序列化方式' + defaultValue: fastjson + isEncrypt: false + + - key: 'client.rm.tccActionInterceptorOrder' + desc: 'tcc 拦截器顺序' + defaultValue: 'Ordered.HIGHEST_PRECEDENCE + 1000' + isEncrypt: false + + - key: 'client.rm.applicationDataLimitCheck' + desc: '客户端应用数据是否开启限制' + defaultValue: false + isEncrypt: false + + - key: 'client.rm.applicationDataLimit' + desc: '客户端应用数据上报限制' + defaultValue: 64000 + isEncrypt: false + + - key: 'client.tm.commitRetryCount' + desc: '一阶段全局提交结果上报 TC 重试次数' + defaultValue: 1 + isEncrypt: false + + - key: 'client.tm.rollbackRetryCount' + desc: '一阶段全局回滚结果上报 TC 重试次数' + defaultValue: 1 + isEncrypt: false + + - key: 'client.tm.defaultGlobalTransactionTimeout' + desc: '全局事务超时时间' + defaultValue: 60 + isEncrypt: false + + - key: 'client.tm.interceptorOrder' + desc: 'TM 全局事务拦截器顺序' + defaultValue: 'Ordered.HIGHEST_PRECEDENCE + 1000' + isEncrypt: false + + - key: 'client.undo.dataValidation' + desc: '二阶段回滚镜像校验' + defaultValue: true + isEncrypt: false + + - key: 'client.undo.logSerialization' + desc: 'undo 序列化方式' + defaultValue: jackson + isEncrypt: false + + - key: 'client.undo.logTable' + desc: '自定义 undo 表名' + defaultValue: undo_log + isEncrypt: false + + - key: 'client.undo.onlyCareUpdateColumns' + desc: '只生成被更新列的镜像' + defaultValue: true + isEncrypt: false + + - key: 'client.undo.compress.enable' + desc: 'undo log 压缩开关' + defaultValue: true + isEncrypt: false + + - key: 'client.undo.compress.type' + desc: 'undo log 压缩算法' + defaultValue: zip + isEncrypt: false + + - key: 'client.undo.compress.threshold' + desc: 'undo log 压缩阈值' + defaultValue: '64k' + isEncrypt: false + + - key: 'client.rm.sqlParserType' + desc: 'sql 解析类型' + defaultValue: druid + isEncrypt: false diff --git a/server/src/test/java/org/apache/seata/server/config/ConfigurationProcessorTest.java b/server/src/test/java/org/apache/seata/server/config/ConfigurationProcessorTest.java new file mode 100644 index 00000000000..589f06ac49f --- /dev/null +++ b/server/src/test/java/org/apache/seata/server/config/ConfigurationProcessorTest.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.server.config; + + +import java.util.HashMap; +import java.util.Map; + +import org.apache.seata.config.dto.ConfigurationItem; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class ConfigurationProcessorTest { + + @Test + void processConfigMap() { + String key1 = "transport.type"; + String value1 = "TCP"; + String key2 = "UNKNOWN"; + String value2 = "UNKNOWN"; + HashMap configMap = new HashMap<>(); + configMap.put(key1, value1); + configMap.put(key2, value2); + + Map itemMap = ConfigurationProcessor.processConfigMap(configMap); + + ConfigurationItem item1 = itemMap.get(key1); + ConfigurationItem item2 = itemMap.get(key2); + Assertions.assertEquals(2, itemMap.size()); + Assertions.assertEquals(key1, item1.getKey()); + Assertions.assertEquals(value1, item1.getValue()); + Assertions.assertNotNull(item1.getDefaultValue()); + Assertions.assertNotNull(item1.getDescription()); + + Assertions.assertEquals(key2, item2.getKey()); + Assertions.assertEquals(value2, item2.getValue()); + Assertions.assertNull(item2.getDefaultValue()); + Assertions.assertNotNull(item2.getDescription()); + + } +} diff --git a/server/src/test/java/org/apache/seata/server/raft/RaftServerTest.java b/server/src/test/java/org/apache/seata/server/raft/RaftServerTest.java index 4633a2d5000..96ea8f81864 100644 --- a/server/src/test/java/org/apache/seata/server/raft/RaftServerTest.java +++ b/server/src/test/java/org/apache/seata/server/raft/RaftServerTest.java @@ -24,10 +24,7 @@ import org.apache.seata.server.lock.LockerManagerFactory; import org.apache.seata.server.session.SessionHolder; import org.apache.seata.server.store.StoreConfig; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; @@ -47,6 +44,11 @@ public static void setUp(ApplicationContext context) { LockerManagerFactory.destroy(); SessionHolder.destroy(); } + @BeforeEach + public void init() { + System.setProperty("server.raftPort", "0"); + System.setProperty(ConfigurationKeys.SERVER_RAFT_SERVER_ADDR, ""); + } @AfterEach public void destroy() { diff --git a/server/src/test/java/org/apache/seata/server/raft/RaftSyncMessageTest.java b/server/src/test/java/org/apache/seata/server/raft/RaftSyncMessageTest.java index 3e36d55a520..05f8392c86a 100644 --- a/server/src/test/java/org/apache/seata/server/raft/RaftSyncMessageTest.java +++ b/server/src/test/java/org/apache/seata/server/raft/RaftSyncMessageTest.java @@ -31,15 +31,18 @@ import org.apache.seata.common.store.SessionMode; import org.apache.seata.core.exception.TransactionException; import org.apache.seata.core.model.BranchType; +import org.apache.seata.server.cluster.raft.execute.config.ConfigOperationType; import org.apache.seata.server.cluster.raft.snapshot.RaftSnapshot; +import org.apache.seata.server.cluster.raft.sync.RaftSyncMessageSerializer; +import org.apache.seata.server.cluster.raft.snapshot.RaftSnapshotSerializer; +import org.apache.seata.server.cluster.raft.snapshot.session.RaftSessionSnapshot; import org.apache.seata.server.cluster.raft.sync.msg.RaftBranchSessionSyncMsg; import org.apache.seata.server.cluster.raft.sync.msg.RaftClusterMetadataMsg; +import org.apache.seata.server.cluster.raft.sync.msg.RaftConfigOperationSyncMsg; import org.apache.seata.server.cluster.raft.sync.msg.RaftGlobalSessionSyncMsg; import org.apache.seata.server.cluster.raft.sync.msg.RaftSyncMessage; -import org.apache.seata.server.cluster.raft.sync.RaftSyncMessageSerializer; -import org.apache.seata.server.cluster.raft.snapshot.RaftSnapshotSerializer; -import org.apache.seata.server.cluster.raft.snapshot.session.RaftSessionSnapshot; import org.apache.seata.server.cluster.raft.sync.msg.dto.BranchTransactionDTO; +import org.apache.seata.server.cluster.raft.sync.msg.dto.ConfigOperationDTO; import org.apache.seata.server.cluster.raft.sync.msg.dto.GlobalTransactionDTO; import org.apache.seata.server.cluster.raft.sync.msg.dto.RaftClusterMetadata; import org.apache.seata.server.session.GlobalSession; @@ -247,4 +250,34 @@ public void testRaftClusterMetadataSerialize() throws IOException { Assertions.assertEquals(ClusterRole.LEARNER,learner1.getRole()); } + @Test + public void testConfigSnapshotSerialize() throws IOException{ + Map configMap = new HashMap<>(); + configMap.put("config.type","file"); + configMap.put("store","file"); + + RaftSnapshot raftSnapshot = new RaftSnapshot(); + raftSnapshot.setBody(configMap); + raftSnapshot.setType(RaftSnapshot.SnapshotType.config); + byte[] msg = RaftSnapshotSerializer.encode(raftSnapshot); + RaftSnapshot raftSnapshot1 = RaftSnapshotSerializer.decode(msg); + HashMap configMap1 = (HashMap) raftSnapshot1.getBody(); + Assertions.assertEquals(configMap,configMap1); + } + + @Test + public void testConfigMsgSerialize() throws IOException{ + RaftSyncMessage raftSyncMessage = new RaftSyncMessage(); + ConfigOperationDTO configOperationDTO = new ConfigOperationDTO(ConfigOperationType.PUT, "namespace", "dataId", "key", "value"); + RaftConfigOperationSyncMsg configSyncMsg = new RaftConfigOperationSyncMsg(configOperationDTO); + raftSyncMessage.setBody(configSyncMsg); + byte[] msg = RaftSyncMessageSerializer.encode(raftSyncMessage); + RaftSyncMessage raftSyncMessage1 = RaftSyncMessageSerializer.decode(msg); + Assertions.assertEquals(configSyncMsg.getMsgType(), ((RaftConfigOperationSyncMsg)raftSyncMessage1.getBody()).getMsgType()); + Assertions.assertEquals(configSyncMsg.getConfigOperation().getKey(), ((RaftConfigOperationSyncMsg)raftSyncMessage1.getBody()).getConfigOperation().getKey()); + Assertions.assertEquals(configSyncMsg.getConfigOperation().getValue(), ((RaftConfigOperationSyncMsg)raftSyncMessage1.getBody()).getConfigOperation().getValue()); + Assertions.assertEquals(configSyncMsg.getConfigOperation().getNamespace(), ((RaftConfigOperationSyncMsg)raftSyncMessage1.getBody()).getConfigOperation().getNamespace()); + Assertions.assertEquals(configSyncMsg.getConfigOperation().getDataId(), ((RaftConfigOperationSyncMsg)raftSyncMessage1.getBody()).getConfigOperation().getDataId()); + Assertions.assertEquals(configSyncMsg.getConfigOperation().getOptType(), ((RaftConfigOperationSyncMsg)raftSyncMessage1.getBody()).getConfigOperation().getOptType()); + } } diff --git a/server/src/test/java/org/apache/seata/server/raft/execute/ConfigOperationExecuteTest.java b/server/src/test/java/org/apache/seata/server/raft/execute/ConfigOperationExecuteTest.java new file mode 100644 index 00000000000..0a7724ce165 --- /dev/null +++ b/server/src/test/java/org/apache/seata/server/raft/execute/ConfigOperationExecuteTest.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.server.raft.execute; + +import javax.annotation.Resource; + +import org.apache.seata.common.ConfigurationKeys; +import org.apache.seata.common.util.NetUtil; +import org.apache.seata.config.ConfigurationCache; +import org.apache.seata.server.cluster.raft.RaftConfigServerManager; +import org.apache.seata.server.controller.ClusterController; +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 org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; + +@SpringBootTest +class ConfigOperationExecuteTest { + @Resource + private ClusterController clusterController; + + private static final String NAMESPACE = "test"; + private static final String DATA_ID = "test"; + + @BeforeAll + public static void setUp(ApplicationContext context) { + RaftConfigServerManager.destroy(); + System.setProperty(ConfigurationKeys.SERVER_RAFT_SERVER_ADDR, NetUtil.getLocalIp() + ":9191"); + System.setProperty("config.type", "raft"); + System.setProperty("registry.preferredNetworks", "*"); + System.setProperty("config.raft.db.type", "rocksdb"); + System.setProperty("config.raft.db.dir", "configStore"); + System.setProperty("config.raft.db.destroyOnShutdown", "true"); + RaftConfigServerManager.init(); + RaftConfigServerManager.start(); + } + + @AfterAll + public static void destroy() { + RaftConfigServerManager.destroy(); + ConfigurationCache.clear(); + System.setProperty(ConfigurationKeys.SERVER_RAFT_SERVER_ADDR,""); + Assertions.assertNull(RaftConfigServerManager.getRaftServer()); + System.clearProperty("config.type"); + System.clearProperty("registry.preferredNetworks"); + System.clearProperty("config.raft.db.dir"); + } + + @Test + public void testCRUD() { + clusterController.deleteAllConfig(NAMESPACE, DATA_ID); + String key1 = "aaa"; + String value1 = "bbb"; + String key2 = "ccc"; + String value2 = "ddd"; + Assertions.assertTrue(clusterController.getConfig(NAMESPACE, DATA_ID, key1).isSuccess()); + Assertions.assertTrue(clusterController.getAllConfig(NAMESPACE, DATA_ID).isSuccess()); + Assertions.assertTrue(clusterController.putConfig(NAMESPACE, DATA_ID, key1, value1).isSuccess()); + Assertions.assertTrue(clusterController.putConfig(NAMESPACE, DATA_ID, key2, value2).isSuccess()); + Assertions.assertTrue(clusterController.deleteConfig(NAMESPACE, DATA_ID, key1).isSuccess()); + Assertions.assertTrue(clusterController.deleteAllConfig(NAMESPACE, DATA_ID).isSuccess()); + Assertions.assertTrue(clusterController.getNamespaces().isSuccess()); + Assertions.assertTrue(clusterController.getDataIds(NAMESPACE).isSuccess()); + } +}