This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 0f08be57eee [refactor](snapshot) Add cloud snapshot handler SPI and
Env override (#66916)
0f08be57eee is described below
commit 0f08be57eee4863e81a575172daee0fb2545e91c
Author: Luwei <[email protected]>
AuthorDate: Thu Aug 20 10:36:11 2026 +0800
[refactor](snapshot) Add cloud snapshot handler SPI and Env override
(#66916)
### What problem does this PR solve?
Issue Number: None
Related PR: selectdb/selectdb-core#9778
Problem Summary: The open-source FE lacks the extension point used by
SelectDB Cloud 4.1 to load the enterprise snapshot handler and to expose
the snapshot-scoped Env. Add the same ServiceLoader fallback and Env
override to master while preserving the configured handler class as the
highest-priority compatibility path.
### Release note
None
### Check List (For Author)
- Test: Unit Test and full FE build
- ./run-fe-ut.sh --run
org.apache.doris.cloud.snapshot.CloudSnapshotHandlerTest
- ./build.sh --fe -j 48
- Behavior changed: No. The default open-source handler and Env behavior
remain unchanged when no extension is installed.
- Does this need documentation: No
---
.../main/java/org/apache/doris/catalog/Env.java | 8 +-
.../doris/cloud/snapshot/CloudSnapshotHandler.java | 36 ++++-
.../cloud/snapshot/CloudSnapshotHandlerTest.java | 145 +++++++++++++++++++++
3 files changed, 183 insertions(+), 6 deletions(-)
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
index 50c1bfc2171..d2d6eea66c7 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
@@ -55,6 +55,7 @@ import org.apache.doris.clone.DynamicPartitionScheduler;
import org.apache.doris.clone.TabletChecker;
import org.apache.doris.clone.TabletScheduler;
import org.apache.doris.clone.TabletSchedulerStat;
+import org.apache.doris.cloud.snapshot.CloudSnapshotHandler;
import org.apache.doris.cloud.system.CloudSystemInfoService;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Config;
@@ -941,9 +942,12 @@ public class Env {
CHECKPOINT = EnvFactory.getInstance().createEnv(true);
}
return CHECKPOINT;
- } else {
- return SingletonHolder.INSTANCE;
}
+ Env snapshotEnv = CloudSnapshotHandler.getSnapshotEnv();
+ if (snapshotEnv != null) {
+ return snapshotEnv;
+ }
+ return SingletonHolder.INSTANCE;
}
// NOTICE: in most case, we should use getCurrentEnv() to get the right
catalog.
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/cloud/snapshot/CloudSnapshotHandler.java
b/fe/fe-core/src/main/java/org/apache/doris/cloud/snapshot/CloudSnapshotHandler.java
index 0b6dc996df5..35905c468db 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/cloud/snapshot/CloudSnapshotHandler.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/cloud/snapshot/CloudSnapshotHandler.java
@@ -17,6 +17,7 @@
package org.apache.doris.cloud.snapshot;
+import org.apache.doris.catalog.Env;
import org.apache.doris.cloud.proto.Cloud;
import org.apache.doris.cloud.rpc.MetaServiceProxy;
import org.apache.doris.common.Config;
@@ -30,24 +31,51 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.lang.reflect.Constructor;
+import java.util.ServiceConfigurationError;
+import java.util.ServiceLoader;
public class CloudSnapshotHandler extends MasterDaemon {
private static final Logger LOG =
LogManager.getLogger(CloudSnapshotHandler.class);
+ private static final String DEFAULT_HANDLER_CLASS =
CloudSnapshotHandler.class.getName();
+ private static volatile Env snapshotEnv;
public CloudSnapshotHandler() {
super("cloud snapshot handler",
Config.cloud_snapshot_handler_interval_second * 1000);
}
public static CloudSnapshotHandler getInstance() {
+ if
(!DEFAULT_HANDLER_CLASS.equals(Config.cloud_snapshot_handler_class)) {
+ return createByClassName(Config.cloud_snapshot_handler_class);
+ }
+ try {
+ for (CloudSnapshotHandler handler :
ServiceLoader.load(CloudSnapshotHandler.class)) {
+ return handler;
+ }
+ } catch (ServiceConfigurationError e) {
+ LOG.error("failed to create cloud snapshot handler from service
loader", e);
+ System.exit(-1);
+ return null;
+ }
+ return new CloudSnapshotHandler();
+ }
+
+ public static Env getSnapshotEnv() {
+ return snapshotEnv;
+ }
+
+ public static void setSnapshotEnv(Env env) {
+ snapshotEnv = env;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static CloudSnapshotHandler createByClassName(String className) {
try {
- Class<CloudSnapshotHandler> theClass =
(Class<CloudSnapshotHandler>) Class.forName(
- Config.cloud_snapshot_handler_class);
+ Class<CloudSnapshotHandler> theClass =
(Class<CloudSnapshotHandler>) Class.forName(className);
Constructor<CloudSnapshotHandler> constructor =
theClass.getDeclaredConstructor();
return constructor.newInstance();
} catch (Exception e) {
- LOG.error("failed to create cloud snapshot handler, class name:
{}", Config.cloud_snapshot_handler_class,
- e);
+ LOG.error("failed to create cloud snapshot handler, class name:
{}", className, e);
System.exit(-1);
return null;
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/cloud/snapshot/CloudSnapshotHandlerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/cloud/snapshot/CloudSnapshotHandlerTest.java
new file mode 100644
index 00000000000..337c2c6b6c0
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/cloud/snapshot/CloudSnapshotHandlerTest.java
@@ -0,0 +1,145 @@
+// 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.doris.cloud.snapshot;
+
+import org.apache.doris.catalog.Env;
+import org.apache.doris.common.Config;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Enumeration;
+
+public class CloudSnapshotHandlerTest {
+
+ private String originalHandlerClass;
+ private ClassLoader originalContextClassLoader;
+
+ @Before
+ public void setUp() {
+ originalHandlerClass = Config.cloud_snapshot_handler_class;
+ originalContextClassLoader =
Thread.currentThread().getContextClassLoader();
+ Config.cloud_snapshot_handler_class =
CloudSnapshotHandler.class.getName();
+ CloudSnapshotHandler.setSnapshotEnv(null);
+ }
+
+ @After
+ public void tearDown() {
+ Config.cloud_snapshot_handler_class = originalHandlerClass;
+
Thread.currentThread().setContextClassLoader(originalContextClassLoader);
+ CloudSnapshotHandler.setSnapshotEnv(null);
+ }
+
+ @Test
+ public void testDefaultHandlerWithoutProvider() {
+ Thread.currentThread().setContextClassLoader(new
SnapshotHandlerClassLoader(null));
+ CloudSnapshotHandler handler = CloudSnapshotHandler.getInstance();
+
+ Assert.assertEquals(CloudSnapshotHandler.class, handler.getClass());
+ }
+
+ @Test
+ public void testLoadHandlerFromServiceProvider() {
+ Thread.currentThread().setContextClassLoader(
+ new
SnapshotHandlerClassLoader(ServiceLoadedSnapshotHandler.class));
+ CloudSnapshotHandler handler = CloudSnapshotHandler.getInstance();
+
+ Assert.assertTrue(handler instanceof ServiceLoadedSnapshotHandler);
+ }
+
+ @Test
+ public void testConfiguredHandlerTakesPrecedenceOverServiceProvider() {
+ Config.cloud_snapshot_handler_class =
ConfiguredSnapshotHandler.class.getName();
+ Thread.currentThread().setContextClassLoader(
+ new
SnapshotHandlerClassLoader(ServiceLoadedSnapshotHandler.class));
+
+ CloudSnapshotHandler handler = CloudSnapshotHandler.getInstance();
+
+ Assert.assertTrue(handler instanceof ConfiguredSnapshotHandler);
+ }
+
+ @Test
+ public void testSnapshotEnvOverridesCurrentEnv() {
+ Env snapshotEnv = Mockito.mock(Env.class);
+
+ CloudSnapshotHandler.setSnapshotEnv(snapshotEnv);
+
+ Assert.assertSame(snapshotEnv, CloudSnapshotHandler.getSnapshotEnv());
+ Assert.assertSame(snapshotEnv, Env.getCurrentEnv());
+ }
+
+ public static class ServiceLoadedSnapshotHandler extends
CloudSnapshotHandler {
+ }
+
+ public static class ConfiguredSnapshotHandler extends CloudSnapshotHandler
{
+ }
+
+ private static class SnapshotHandlerClassLoader extends ClassLoader {
+
+ private static final String SERVICE_FILE =
+ "META-INF/services/" + CloudSnapshotHandler.class.getName();
+
+ private final Class<? extends CloudSnapshotHandler> providerClass;
+
+ SnapshotHandlerClassLoader(Class<? extends CloudSnapshotHandler>
providerClass) {
+ super(CloudSnapshotHandlerTest.class.getClassLoader());
+ this.providerClass = providerClass;
+ }
+
+ @Override
+ public Enumeration<URL> getResources(String name) {
+ if (!SERVICE_FILE.equals(name) || providerClass == null) {
+ return Collections.emptyEnumeration();
+ }
+ return
Collections.enumeration(Collections.singletonList(serviceFileUrl()));
+ }
+
+ private URL serviceFileUrl() {
+ byte[] content =
providerClass.getName().getBytes(StandardCharsets.UTF_8);
+ try {
+ return new URL("synthetic", "", -1, SERVICE_FILE, new
URLStreamHandler() {
+ @Override
+ protected URLConnection openConnection(URL url) {
+ return new URLConnection(url) {
+ @Override
+ public void connect() {
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new ByteArrayInputStream(content);
+ }
+ };
+ }
+ });
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]