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 6ddb9af9057 [fix](catalog) Make external catalog resource cleanup
ownership-safe (#66269)
6ddb9af9057 is described below
commit 6ddb9af90571d5ceb41d13caacd80ebbf753b913
Author: Socrates <[email protected]>
AuthorDate: Wed Aug 5 10:01:47 2026 +0800
[fix](catalog) Make external catalog resource cleanup ownership-safe
(#66269)
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary:
External catalog creation can allocate resources before the catalog is
registered, including temporary JDBC clients, connection pools, and
access-controller plugins. Those temporary objects did not have a
consistent owner on validation failure or dry-run paths, so they could
outlive the catalog attempt.
This change makes cleanup ownership explicit:
- Separates unregistered-catalog creation-failure cleanup from
registered catalog teardown.
- Closes temporary JDBC clients after OceanBase compatibility detection
while preserving the returned normal-use client.
- Adds an AccessController lifecycle hook and closes dry-run controllers
and controllers removed with a catalog.
- Makes Ranger Hive controller shutdown safe: scheduled audit flushing
is cancelled, pending audit events are drained once, and plugin cleanup
waits for in-flight authorization.
- Corrects the Ranger audit flusher so each scheduled invocation
performs one flush instead of owning an infinite loop.
---
.../connector/jdbc/client/JdbcConnectorClient.java | 21 ++-
.../ranger/hive/RangerHiveAccessController.java | 110 +++++++++++--
.../ranger/hive/RangerHiveAuditHandler.java | 53 ++++++-
.../ranger/hive/RangerHiveAuditLogFlusher.java | 17 +-
.../apache/doris/datasource/CatalogFactory.java | 26 ++-
.../org/apache/doris/datasource/CatalogIf.java | 5 +
.../org/apache/doris/datasource/CatalogMgr.java | 90 ++++++-----
.../apache/doris/datasource/ExternalCatalog.java | 70 ++++++++-
.../jdbc/client/JdbcOceanBaseClient.java | 1 +
.../plugin/PluginDrivenExternalCatalog.java | 22 ++-
.../apache/doris/job/util/StreamingJobUtils.java | 174 +++++++++++----------
.../mysql/privilege/AccessControllerManager.java | 153 +++++++++++++++---
.../mysql/privilege/CatalogAccessController.java | 3 +
.../ranger/hive/RangerHiveAuditLogFlusherTest.java | 139 ++++++++++++++++
.../doris/datasource/CatalogFactoryTest.java | 90 +++++++++++
.../apache/doris/datasource/CatalogMgrTest.java | 60 +++++++
.../datasource/ExternalCatalogDeadlockTest.java | 17 +-
.../jdbc/client/JdbcOceanBaseClientTest.java | 105 +++++++++++++
.../doris/job/util/StreamingJobUtilsTest.java | 20 +++
.../privilege/AccessControllerManagerTest.java | 162 +++++++++++++++++++
20 files changed, 1133 insertions(+), 205 deletions(-)
diff --git
a/fe/fe-connector/fe-connector-jdbc/src/main/java/org/apache/doris/connector/jdbc/client/JdbcConnectorClient.java
b/fe/fe-connector/fe-connector-jdbc/src/main/java/org/apache/doris/connector/jdbc/client/JdbcConnectorClient.java
index a3871e0005d..e1ad6c33b52 100644
---
a/fe/fe-connector/fe-connector-jdbc/src/main/java/org/apache/doris/connector/jdbc/client/JdbcConnectorClient.java
+++
b/fe/fe-connector/fe-connector-jdbc/src/main/java/org/apache/doris/connector/jdbc/client/JdbcConnectorClient.java
@@ -179,12 +179,21 @@ public abstract class JdbcConnectorClient implements
Closeable {
default:
throw new DorisConnectorException("Unsupported JDBC DB type: "
+ dbType);
}
- client.initializeClassLoader(driverUrl);
- String sanitizedUrl = urlSanitizer.apply(jdbcUrl);
- client.initializeDataSource(sanitizedUrl, user, password, driverClass,
- poolMinSize, poolMaxSize, poolMaxWaitTime, poolMaxLifeTime);
- client.postInitialize();
- return client;
+ try {
+ client.initializeClassLoader(driverUrl);
+ String sanitizedUrl = urlSanitizer.apply(jdbcUrl);
+ client.initializeDataSource(sanitizedUrl, user, password,
driverClass,
+ poolMinSize, poolMaxSize, poolMaxWaitTime,
poolMaxLifeTime);
+ client.postInitialize();
+ return client;
+ } catch (RuntimeException | Error e) {
+ try {
+ client.close();
+ } catch (RuntimeException | Error closeFailure) {
+ e.addSuppressed(closeFailure);
+ }
+ throw e;
+ }
}
protected JdbcConnectorClient(
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAccessController.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAccessController.java
index ae86e1ff14f..6f862ceade1 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAccessController.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAccessController.java
@@ -24,7 +24,9 @@ import
org.apache.doris.catalog.authorizer.ranger.RangerAccessController;
import org.apache.doris.common.AuthorizationException;
import org.apache.doris.common.ThreadPoolManager;
import org.apache.doris.datasource.InternalCatalog;
+import org.apache.doris.mysql.privilege.DataMaskPolicy;
import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.mysql.privilege.RowFilterPolicy;
import com.google.common.collect.Maps;
import org.apache.logging.log4j.LogManager;
@@ -42,17 +44,25 @@ import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
public class RangerHiveAccessController extends RangerAccessController {
private static final Logger LOG =
LogManager.getLogger(RangerHiveAccessController.class);
- private static ScheduledThreadPoolExecutor logFlushTimer =
ThreadPoolManager.newDaemonScheduledThreadPool(1,
+ private static final ScheduledThreadPoolExecutor LOG_FLUSH_TIMER =
ThreadPoolManager.newDaemonScheduledThreadPool(1,
"ranger-hive-audit-log-flusher-timer", true);
private RangerHivePlugin hivePlugin;
private RangerHiveAuditHandler auditHandler;
+ private ScheduledFuture<?> logFlushFuture;
+ // The manager can remove a controller while a query still holds its
reference. Keep the plugin alive
+ // until that query completes, then prevent any later authorization from
using the cleaned plugin.
+ private final ReentrantReadWriteLock lifecycleLock = new
ReentrantReadWriteLock();
+ private boolean closed;
public RangerHiveAccessController(Map<String, String> properties) {
this(properties, null);
@@ -64,7 +74,41 @@ public class RangerHiveAccessController extends
RangerAccessController {
hivePlugin = new RangerHivePlugin(serviceName,
rangerAuthContextListener);
auditHandler = new RangerHiveAuditHandler(hivePlugin.getConfig());
// start a timed log flusher
- logFlushTimer.scheduleAtFixedRate(new
RangerHiveAuditLogFlusher(auditHandler), 10, 20L, TimeUnit.SECONDS);
+ logFlushFuture = LOG_FLUSH_TIMER.scheduleAtFixedRate(
+ new RangerHiveAuditLogFlusher(auditHandler), 10, 20L,
TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void close() {
+ lifecycleLock.writeLock().lock();
+ try {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ if (logFlushFuture != null) {
+ logFlushFuture.cancel(false);
+ logFlushFuture = null;
+ }
+ // flushAudit atomically drains the handler. This preserves events
produced before close without
+ // racing the periodic flusher or re-emitting events it has
already sent.
+ try {
+ auditHandler.flushAudit();
+ } catch (Throwable e) {
+ LOG.warn("Failed to flush Ranger Hive audit events while
closing the access controller", e);
+ }
+ if (hivePlugin != null) {
+ try {
+ hivePlugin.cleanup();
+ } catch (Throwable e) {
+ LOG.warn("Failed to clean up Ranger Hive plugin", e);
+ } finally {
+ hivePlugin = null;
+ }
+ }
+ } finally {
+ lifecycleLock.writeLock().unlock();
+ }
}
private RangerAccessRequestImpl createRequest(UserIdentity currentUser,
HiveAccessType accessType) {
@@ -95,24 +139,40 @@ public class RangerHiveAccessController extends
RangerAccessController {
private void checkPrivileges(UserIdentity currentUser, HiveAccessType
accessType,
List<RangerHiveResource> hiveResources) throws
AuthorizationException {
- List<RangerAccessRequest> requests = new ArrayList<>();
- for (RangerHiveResource resource : hiveResources) {
- RangerAccessRequestImpl request = createRequest(currentUser,
accessType);
- request.setResource(resource);
- requests.add(request);
- }
+ lifecycleLock.readLock().lock();
+ try {
+ if (closed) {
+ throw new AuthorizationException("Ranger Hive access
controller has been closed");
+ }
+ List<RangerAccessRequest> requests = new ArrayList<>();
+ for (RangerHiveResource resource : hiveResources) {
+ RangerAccessRequestImpl request = createRequest(currentUser,
accessType);
+ request.setResource(resource);
+ requests.add(request);
+ }
- Collection<RangerAccessResult> results =
hivePlugin.isAccessAllowed(requests, auditHandler);
- checkRequestResults(results, accessType.name());
+ Collection<RangerAccessResult> results =
hivePlugin.isAccessAllowed(requests, auditHandler);
+ checkRequestResults(results, accessType.name());
+ } finally {
+ lifecycleLock.readLock().unlock();
+ }
}
private boolean checkPrivilege(UserIdentity currentUser, HiveAccessType
accessType,
RangerHiveResource resource) {
- RangerAccessRequestImpl request = createRequest(currentUser,
accessType);
- request.setResource(resource);
+ lifecycleLock.readLock().lock();
+ try {
+ if (closed) {
+ return false;
+ }
+ RangerAccessRequestImpl request = createRequest(currentUser,
accessType);
+ request.setResource(resource);
- RangerAccessResult result = hivePlugin.isAccessAllowed(request,
auditHandler);
- return checkRequestResult(request, result, accessType.name());
+ RangerAccessResult result = hivePlugin.isAccessAllowed(request,
auditHandler);
+ return checkRequestResult(request, result, accessType.name());
+ } finally {
+ lifecycleLock.readLock().unlock();
+ }
}
private HiveAccessType convertToAccessType(PrivPredicate predicate) {
@@ -198,6 +258,28 @@ public class RangerHiveAccessController extends
RangerAccessController {
return true;
}
+ @Override
+ public List<? extends RowFilterPolicy> evalRowFilterPolicies(UserIdentity
currentUser, String ctl, String db,
+ String tbl) {
+ lifecycleLock.readLock().lock();
+ try {
+ return closed ? new ArrayList<>() :
super.evalRowFilterPolicies(currentUser, ctl, db, tbl);
+ } finally {
+ lifecycleLock.readLock().unlock();
+ }
+ }
+
+ @Override
+ public Optional<DataMaskPolicy> evalDataMaskPolicy(UserIdentity
currentUser, String ctl, String db, String tbl,
+ String col) {
+ lifecycleLock.readLock().lock();
+ try {
+ return closed ? Optional.empty() :
super.evalDataMaskPolicy(currentUser, ctl, db, tbl, col);
+ } finally {
+ lifecycleLock.readLock().unlock();
+ }
+ }
+
@Override
protected RangerHiveResource createResource(String ctl, String db, String
tbl) {
return new RangerHiveResource(HiveObjectType.TABLE,
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAuditHandler.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAuditHandler.java
index 73d73a3933b..276c1155cbe 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAuditHandler.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAuditHandler.java
@@ -29,9 +29,11 @@ import
org.apache.ranger.plugin.policyengine.RangerAccessResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -66,7 +68,12 @@ public class RangerHiveAuditHandler extends
RangerDefaultAuditHandler {
private static final Logger LOG =
LoggerFactory.getLogger(RangerDefaultAuditHandler.class);
private final int requestQuerySize;
+ private final Object auditBufferLock = new Object();
+ private final Object flushLock = new Object();
private final Collection<AuthzAuditEvent> auditEvents = new ArrayList<>();
+ // Only accessed while holding flushLock. Successfully delivered events
are removed one by one, so a
+ // provider exception leaves the failed event and every later event
available for the next periodic tick.
+ private final Deque<AuthzAuditEvent> pendingAuditEvents = new
ArrayDeque<>();
private boolean deniedExists = false;
public RangerHiveAuditHandler() {
@@ -240,17 +247,43 @@ public class RangerHiveAuditHandler extends
RangerDefaultAuditHandler {
}
public void flushAudit() {
- for (AuthzAuditEvent auditEvent : auditEvents) {
- if (deniedExists && auditEvent.getAccessResult() != 0) { // if
deny exists, skip logging for allowed results
- continue;
+ synchronized (flushLock) {
+ Collection<AuthzAuditEvent> eventsToFlush;
+ boolean deniedExistsForEvents;
+ // Keep the producer critical section limited to snapshot/reset.
Provider delivery can block, but
+ // authorization callbacks remain free to enqueue into the next
batch.
+ synchronized (auditBufferLock) {
+ eventsToFlush = new ArrayList<>(auditEvents);
+ deniedExistsForEvents = deniedExists;
+ auditEvents.clear();
+ deniedExists = false;
}
- super.logAuthzAudit(auditEvent);
+ for (AuthzAuditEvent auditEvent : eventsToFlush) {
+ // If a deny exists, skip logging allowed results from the
same drained batch.
+ if (!deniedExistsForEvents || auditEvent.getAccessResult() ==
0) {
+ pendingAuditEvents.addLast(auditEvent);
+ }
+ }
+
+ while (!pendingAuditEvents.isEmpty()) {
+ AuthzAuditEvent auditEvent = pendingAuditEvents.peekFirst();
+ logAuditEvent(auditEvent);
+ // Remove only after the provider confirms delivery by
returning normally.
+ pendingAuditEvents.removeFirst();
+ }
}
}
- private void addAuthzAuditEvent(AuthzAuditEvent auditEvent) {
- if (auditEvent != null) {
+ protected void logAuditEvent(AuthzAuditEvent auditEvent) {
+ super.logAuthzAudit(auditEvent);
+ }
+
+ void addAuthzAuditEvent(AuthzAuditEvent auditEvent) {
+ if (auditEvent == null) {
+ return;
+ }
+ synchronized (auditBufferLock) {
auditEvents.add(auditEvent);
if (auditEvent.getAccessResult() == 0) {
@@ -259,6 +292,14 @@ public class RangerHiveAuditHandler extends
RangerDefaultAuditHandler {
}
}
+ int getPendingAuditEventCountForTest() {
+ synchronized (flushLock) {
+ synchronized (auditBufferLock) {
+ return pendingAuditEvents.size() + auditEvents.size();
+ }
+ }
+ }
+
private boolean skipFilterOperationAuditing(RangerAccessResult result) {
boolean ret = false;
RangerAccessRequest accessRequest = result.getAccessRequest();
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAuditLogFlusher.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAuditLogFlusher.java
index e8afda11462..ffcd26d6667 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAuditLogFlusher.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAuditLogFlusher.java
@@ -17,12 +17,13 @@
package org.apache.doris.catalog.authorizer.ranger.hive;
-import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.TimerTask;
-@Slf4j
public class RangerHiveAuditLogFlusher extends TimerTask {
+ private static final Logger LOG =
LoggerFactory.getLogger(RangerHiveAuditLogFlusher.class);
private RangerHiveAuditHandler auditHandler;
public RangerHiveAuditLogFlusher(RangerHiveAuditHandler auditHandler) {
@@ -31,14 +32,12 @@ public class RangerHiveAuditLogFlusher extends TimerTask {
@Override
public void run() {
- while (true) {
+ try {
this.auditHandler.flushAudit();
-
- try {
- Thread.sleep(20000);
- } catch (InterruptedException e) {
- log.info("error ", e);
- }
+ } catch (Throwable t) {
+ // ScheduledThreadPoolExecutor suppresses every later fixed-rate
invocation when one run escapes with
+ // an exception. Keep the periodic flusher alive;
RangerHiveAuditHandler retains undelivered events.
+ LOG.warn("Failed to flush Ranger Hive audit events; will retry on
the next tick", t);
}
}
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogFactory.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogFactory.java
index 977840ef94f..a781fcc9eaa 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogFactory.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogFactory.java
@@ -175,11 +175,20 @@ public class CatalogFactory {
}
}
- // set some default properties if missing when creating catalog.
- // both replaying the creating logic will call this method.
- catalog.setDefaultPropsIfMissing(isReplay);
+ return finishCatalogCreation(catalog, isReplay);
+ }
+
+ static ExternalCatalog finishCatalogCreation(ExternalCatalog catalog,
boolean isReplay) throws DdlException {
+ // Set some default properties if missing when creating catalog.
+ // Both replaying the creating logic will call this method.
+ if (isReplay) {
+ catalog.setDefaultPropsIfMissing(true);
+ return catalog;
+ }
- if (!isReplay) {
+ boolean creationFinished = false;
+ try {
+ catalog.setDefaultPropsIfMissing(false);
catalog.checkWhenCreating();
// This will check if the customized access controller can be
created successfully.
// If failed, it will throw exception and the catalog will not be
created.
@@ -189,9 +198,12 @@ public class CatalogFactory {
LOG.warn("Failed to init access controller", e);
throw new DdlException("Failed to init access controller: " +
e.getMessage());
}
+ creationFinished = true;
+ return catalog;
+ } finally {
+ if (!creationFinished) {
+ catalog.onCreateFailure();
+ }
}
- return catalog;
}
}
-
-
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogIf.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogIf.java
index 1751ac107a6..e0ded1edc67 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogIf.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogIf.java
@@ -162,6 +162,11 @@ public interface CatalogIf<T extends DatabaseIf> {
// Called when catalog is dropped
void onClose();
+ // Called when catalog creation fails before the catalog is registered.
+ default void onCreateFailure() {
+ onClose();
+ }
+
String getComment();
default void setComment(String comment) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogMgr.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogMgr.java
index 0417e01f7f0..176d669f81d 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogMgr.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/CatalogMgr.java
@@ -252,7 +252,7 @@ public class CatalogMgr implements Writable,
GsonPostProcessable {
try {
if (nameToCatalog.containsKey(catalog.getName())) {
// Close the already-constructed catalog to release connector
resources.
- catalog.onClose();
+ catalog.onCreateFailure();
if (ifNotExists) {
LOG.warn("Catalog {} is already exist.", catalogName);
return;
@@ -410,6 +410,7 @@ public class CatalogMgr implements Writable,
GsonPostProcessable {
* Modify the catalog property and write the meta log.
*/
public void alterCatalogProps(String catalogName, Map<String, String>
newProperties) throws UserException {
+ Runnable accessControllerCleanup = () -> { };
writeLock();
try {
CatalogIf catalog = nameToCatalog.get(catalogName);
@@ -424,10 +425,11 @@ public class CatalogMgr implements Writable,
GsonPostProcessable {
CatalogLog log = new CatalogLog();
log.setCatalogId(catalog.getId());
log.setNewProps(newProperties);
- replayAlterCatalogProps(log, oldProperties, false);
+ accessControllerCleanup = applyAlterCatalogProps(log,
oldProperties, false, true);
Env.getCurrentEnv().getEditLog().logCatalogLog(OperationType.OP_ALTER_CATALOG_PROPS,
log);
} finally {
writeUnlock();
+ accessControllerCleanup.run();
}
}
@@ -642,48 +644,60 @@ public class CatalogMgr implements Writable,
GsonPostProcessable {
*/
public void replayAlterCatalogProps(CatalogLog log, Map<String, String>
oldProperties, boolean isReplay)
throws DdlException {
+ Runnable accessControllerCleanup = () -> { };
writeLock();
try {
- CatalogIf catalog = idToCatalog.get(log.getCatalogId());
- if (catalog instanceof ExternalCatalog) {
- Map<String, String> newProps = log.getNewProps();
- if (!isReplay) {
- boolean tentativelyMutated = false;
- try {
- ExternalCatalog externalCatalog = (ExternalCatalog)
catalog;
- boolean validatedWithoutMutation =
externalCatalog.validatePropertiesBeforeUpdate(
- oldProperties, newProps);
- if (!validatedWithoutMutation) {
- externalCatalog.tryModifyCatalogProps(newProps);
- tentativelyMutated = true;
- externalCatalog.checkProperties();
- }
- } catch (Exception validationException) {
- // Only legacy validators publish a tentative
candidate. Detached validators
- // leave the live CatalogProperty untouched while
concurrent initialization runs.
- if (oldProperties != null && tentativelyMutated) {
- ((ExternalCatalog)
catalog).rollBackCatalogProps(oldProperties);
- }
- if (validationException instanceof DdlException) {
- throw (DdlException) validationException;
- }
- throw new DdlException("Invalid catalog properties: "
- + validationException.getMessage(),
validationException);
+ accessControllerCleanup = applyAlterCatalogProps(log,
oldProperties, isReplay, true);
+ } finally {
+ writeUnlock();
+ accessControllerCleanup.run();
+ }
+ }
+
+ private Runnable applyAlterCatalogProps(CatalogLog log, Map<String,
String> oldProperties,
+ boolean isReplay, boolean deferAccessControllerCleanup) throws
DdlException {
+ CatalogIf catalog = idToCatalog.get(log.getCatalogId());
+ if (catalog instanceof ExternalCatalog) {
+ Map<String, String> newProps = log.getNewProps();
+ if (!isReplay) {
+ boolean tentativelyMutated = false;
+ try {
+ ExternalCatalog externalCatalog = (ExternalCatalog)
catalog;
+ boolean validatedWithoutMutation =
externalCatalog.validatePropertiesBeforeUpdate(
+ oldProperties, newProps);
+ if (!validatedWithoutMutation) {
+ externalCatalog.tryModifyCatalogProps(newProps);
+ tentativelyMutated = true;
+ externalCatalog.checkProperties();
}
- } else {
- ((ExternalCatalog)
catalog).tryModifyCatalogProps(newProps);
- }
- if (newProps.containsKey(METADATA_REFRESH_INTERVAL_SEC)) {
- long catalogId = catalog.getId();
- Integer metadataRefreshIntervalSec =
Integer.valueOf(newProps.get(METADATA_REFRESH_INTERVAL_SEC));
- Integer[] sec = {metadataRefreshIntervalSec,
metadataRefreshIntervalSec};
-
Env.getCurrentEnv().getRefreshManager().addToRefreshMap(catalogId, sec);
+ } catch (Exception validationException) {
+ // Only legacy validators publish a tentative candidate.
Detached validators
+ // leave the live CatalogProperty untouched while
concurrent initialization runs.
+ if (oldProperties != null && tentativelyMutated) {
+ ((ExternalCatalog)
catalog).rollBackCatalogProps(oldProperties);
+ }
+ if (validationException instanceof DdlException) {
+ throw (DdlException) validationException;
+ }
+ throw new DdlException("Invalid catalog properties: "
+ + validationException.getMessage(),
validationException);
}
+ } else {
+ ((ExternalCatalog) catalog).tryModifyCatalogProps(newProps);
+ }
+ if (newProps.containsKey(METADATA_REFRESH_INTERVAL_SEC)) {
+ long catalogId = catalog.getId();
+ Integer metadataRefreshIntervalSec =
Integer.valueOf(newProps.get(METADATA_REFRESH_INTERVAL_SEC));
+ Integer[] sec = {metadataRefreshIntervalSec,
metadataRefreshIntervalSec};
+
Env.getCurrentEnv().getRefreshManager().addToRefreshMap(catalogId, sec);
+ }
+ if (deferAccessControllerCleanup) {
+ return ((ExternalCatalog) catalog)
+
.modifyCatalogPropsWithDeferredAccessControllerCleanup(log.getNewProps());
}
- catalog.modifyCatalogProps(log.getNewProps());
- } finally {
- writeUnlock();
}
+ catalog.modifyCatalogProps(log.getNewProps());
+ return () -> { };
}
public void unregisterExternalTable(String dbName, String tableName,
String catalogName, boolean ignoreIfExists)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
index 82048d0f0ed..86476b830da 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
@@ -496,7 +496,7 @@ public abstract class ExternalCatalog
}
// 3. create access controller
- Env.getCurrentEnv().getAccessManager().createAccessController(name,
className, acProperties, isDryRun);
+ Env.getCurrentEnv().getAccessManager().createAccessController(this,
className, acProperties, isDryRun);
}
/**
@@ -599,12 +599,28 @@ public abstract class ExternalCatalog
* and reloaded during the refresh process.
*/
public void resetToUninitialized(boolean invalidCache) {
+ resetToUninitialized(invalidCache, false);
+ }
+
+ private Runnable resetToUninitialized(boolean invalidCache, boolean
deferAccessControllerCleanup) {
+ Runnable accessControllerCleanup;
synchronized (this) {
this.objectCreated = false;
this.initialized = false;
- onClose();
+ accessControllerCleanup = detachAccessController();
+ closeResourcesQuietly("resetting catalog");
+ }
+ try {
+ onRefreshCache(invalidCache);
+ } catch (RuntimeException | Error e) {
+ accessControllerCleanup.run();
+ throw e;
}
- onRefreshCache(invalidCache);
+ if (!deferAccessControllerCleanup) {
+ accessControllerCleanup.run();
+ return () -> { };
+ }
+ return accessControllerCleanup;
}
/**
@@ -832,6 +848,25 @@ public abstract class ExternalCatalog
notifyPropertiesUpdated(props);
}
+ /**
+ * Apply property changes while atomically detaching the old access
controller, but return its potentially
+ * blocking close operation to CatalogMgr so it can run after releasing
the global catalog write lock.
+ */
+ public Runnable
modifyCatalogPropsWithDeferredAccessControllerCleanup(Map<String, String>
props) {
+ catalogProperty.modifyCatalogProps(props);
+ Runnable accessControllerCleanup = resetToUninitialized(false, true);
+ try {
+ String schemaCacheTtl =
props.getOrDefault(SCHEMA_CACHE_TTL_SECOND, null);
+ if (java.util.Objects.nonNull(schemaCacheTtl)) {
+ Env.getCurrentEnv().getExtMetaCacheMgr().removeCatalog(id);
+ }
+ return accessControllerCleanup;
+ } catch (RuntimeException | Error e) {
+ accessControllerCleanup.run();
+ throw e;
+ }
+ }
+
public void tryModifyCatalogProps(Map<String, String> props) {
catalogProperty.modifyCatalogProps(props);
}
@@ -849,8 +884,29 @@ public abstract class ExternalCatalog
}
@Override
- public void onClose() {
- removeAccessController();
+ public final void onClose() {
+ Runnable accessControllerCleanup = detachAccessController();
+ try {
+ closeResourcesQuietly("registered catalog");
+ } finally {
+ accessControllerCleanup.run();
+ }
+ }
+
+ @Override
+ public final void onCreateFailure() {
+ closeResourcesQuietly("unregistered catalog");
+ }
+
+ private void closeResourcesQuietly(String lifecycleStage) {
+ try {
+ closeResources();
+ } catch (Throwable t) {
+ LOG.warn("Failed to close resources for {} {}", lifecycleStage,
name, t);
+ }
+ }
+
+ protected void closeResources() {
if (threadPoolWithPreAuth != null) {
ThreadPoolManager.shutdownExecutorService(threadPoolWithPreAuth);
}
@@ -862,8 +918,8 @@ public abstract class ExternalCatalog
}
}
- private void removeAccessController() {
- Env.getCurrentEnv().getAccessManager().removeAccessController(name);
+ private Runnable detachAccessController() {
+ return
Env.getCurrentEnv().getAccessManager().detachAccessController(name, id);
}
/**
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/client/JdbcOceanBaseClient.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/client/JdbcOceanBaseClient.java
index 004e7c80f3d..8dda7721335 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/client/JdbcOceanBaseClient.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/client/JdbcOceanBaseClient.java
@@ -57,6 +57,7 @@ public class JdbcOceanBaseClient extends JdbcClient {
throw new JdbcClientException("Failed to initialize
JdbcOceanBaseClient: %s", e.getMessage());
} finally {
close(rs, stmt, conn);
+ closeClient();
}
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenExternalCatalog.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenExternalCatalog.java
index e228fca35a7..657d8dfbf8a 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenExternalCatalog.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenExternalCatalog.java
@@ -1513,21 +1513,29 @@ public class PluginDrivenExternalCatalog extends
ExternalCatalog {
}
try {
context.close();
- } catch (IOException e) {
+ } catch (Throwable e) {
LOG.warn("Failed to close connector context filesystem for catalog
{}", name, e);
}
}
@Override
- public void onClose() {
- super.onClose();
- if (connector != null) {
+ protected void closeResources() {
+ try {
+ super.closeResources();
+ } catch (Throwable e) {
+ LOG.warn("Failed to close common resources for plugin-driven
catalog {}", name, e);
+ }
+
+ // Detach every stage before invoking external code. A throwing
connector must not remain reachable
+ // for another close attempt or prevent the connector-context stage
from running.
+ Connector connectorToClose = connector;
+ connector = null;
+ if (connectorToClose != null) {
try {
- connector.close();
- } catch (IOException e) {
+ connectorToClose.close();
+ } catch (Throwable e) {
LOG.warn("Failed to close connector for catalog {}", name, e);
}
- connector = null;
}
// Close the shared context's cached engine FileSystem AFTER the
connector(s) release their borrowed
// reference to it. No-op when no FS was ever built (e.g. non-hive
plugin catalogs never call
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/job/util/StreamingJobUtils.java
b/fe/fe-core/src/main/java/org/apache/doris/job/util/StreamingJobUtils.java
index 1ef6bf18093..0877e43a698 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/job/util/StreamingJobUtils.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/job/util/StreamingJobUtils.java
@@ -379,98 +379,102 @@ public class StreamingJobUtils {
}
JdbcClient jdbcClient = getJdbcClient(sourceType, properties);
- String database = getRemoteDbName(sourceType, properties);
- List<String> tablesNameList = jdbcClient.getTablesNameList(database);
- if (tablesNameList.isEmpty()) {
- throw new JobException("No tables found in database " + database);
- }
- Map<String, String> tableCreateProperties =
getTableCreateProperties(targetProperties);
-
- List<String> noPrimaryKeyTables = new ArrayList<>();
- for (String table : tablesNameList) {
- if (!includeTablesList.isEmpty() &&
!includeTablesList.contains(table)) {
- log.info("Skip table {} in database {} as it does not in
include_tables {}", table, database,
- includeTables);
- continue;
+ try {
+ String database = getRemoteDbName(sourceType, properties);
+ List<String> tablesNameList =
jdbcClient.getTablesNameList(database);
+ if (tablesNameList.isEmpty()) {
+ throw new JobException("No tables found in database " +
database);
}
+ Map<String, String> tableCreateProperties =
getTableCreateProperties(targetProperties);
+
+ List<String> noPrimaryKeyTables = new ArrayList<>();
+ for (String table : tablesNameList) {
+ if (!includeTablesList.isEmpty() &&
!includeTablesList.contains(table)) {
+ log.info("Skip table {} in database {} as it does not in
include_tables {}", table, database,
+ includeTables);
+ continue;
+ }
- // if set include_tables, exclude_tables is ignored
- if (includeTablesList.isEmpty()
- && !excludeTablesList.isEmpty() &&
excludeTablesList.contains(table)) {
- log.info("Skip table {} in database {} as it in exclude_tables
{}", table, database,
- excludeTables);
- continue;
- }
+ // if set include_tables, exclude_tables is ignored
+ if (includeTablesList.isEmpty()
+ && !excludeTablesList.isEmpty() &&
excludeTablesList.contains(table)) {
+ log.info("Skip table {} in database {} as it in
exclude_tables {}", table, database,
+ excludeTables);
+ continue;
+ }
- List<String> primaryKeys = jdbcClient.getPrimaryKeys(database,
table);
- List<Column> columns = getColumns(jdbcClient, database, table,
primaryKeys);
- if (primaryKeys.isEmpty()) {
- noPrimaryKeyTables.add(table);
- }
+ List<String> primaryKeys = jdbcClient.getPrimaryKeys(database,
table);
+ List<Column> columns = getColumns(jdbcClient, database, table,
primaryKeys);
+ if (primaryKeys.isEmpty()) {
+ noPrimaryKeyTables.add(table);
+ }
- // Resolve target (Doris) table name; defaults to source table
name if not configured
- String targetTableName = properties.getOrDefault(
- DataSourceConfigKeys.TABLE + "." + table + "."
- + DataSourceConfigKeys.TABLE_TARGET_TABLE_SUFFIX,
- table).trim();
-
- // Validate and apply exclude_columns for this table
- Set<String> excludeColumns = parseExcludeColumns(properties,
table);
- if (!excludeColumns.isEmpty()) {
- validateExcludeColumns(excludeColumns, table, columns,
primaryKeys);
- columns = columns.stream()
- .filter(col -> !excludeColumns.contains(col.getName()))
- .collect(Collectors.toList());
- }
+ // Resolve target (Doris) table name; defaults to source table
name if not configured
+ String targetTableName = properties.getOrDefault(
+ DataSourceConfigKeys.TABLE + "." + table + "."
+ +
DataSourceConfigKeys.TABLE_TARGET_TABLE_SUFFIX,
+ table).trim();
+
+ // Validate and apply exclude_columns for this table
+ Set<String> excludeColumns = parseExcludeColumns(properties,
table);
+ if (!excludeColumns.isEmpty()) {
+ validateExcludeColumns(excludeColumns, table, columns,
primaryKeys);
+ columns = columns.stream()
+ .filter(col ->
!excludeColumns.contains(col.getName()))
+ .collect(Collectors.toList());
+ }
- // Convert Column to ColumnDefinition
- List<ColumnDefinition> columnDefinitions =
columns.stream().map(col -> {
- DataType dataType = DataType.fromCatalogType(col.getType());
- return new ColumnDefinition(col.getName(), dataType,
col.isAllowNull(), col.getComment());
- }).collect(Collectors.toList());
-
- // Create DistributionDescriptor
- DistributionDescriptor distribution = new DistributionDescriptor(
- true, // isHash
- true, // isAutoBucket
- FeConstants.default_bucket_num,
- primaryKeys
- );
-
- // Create CreateTableInfo
- CreateTableInfo createtblInfo = new CreateTableInfo(
- true, // ifNotExists
- false, // isExternal
- false, // isTemp
- InternalCatalog.INTERNAL_CATALOG_NAME, // ctlName
- targetDb, // dbName
- targetTableName, // tableName
- columnDefinitions, // columns
- ImmutableList.of(), // indexes
- "olap", // engineName
- KeysType.UNIQUE_KEYS, // keysType
- primaryKeys, // keys
- "", // comment
- PartitionTableInfo.EMPTY, // partitionTableInfo
- distribution, // distribution
- ImmutableList.of(), // rollups
- new HashMap<>(tableCreateProperties), // properties
- ImmutableMap.of(), // extProperties
- ImmutableList.of() // clusterKeyColumnNames
- );
- CreateTableCommand createtblCmd = new
CreateTableCommand(Optional.empty(), createtblInfo);
- // Key: source (PG/MySQL) table name; Value: command that creates
the Doris target table
- createtblCmds.put(table, createtblCmd);
- }
- if (createtblCmds.isEmpty()) {
- throw new JobException("Can not found match table in database " +
database);
- }
+ // Convert Column to ColumnDefinition
+ List<ColumnDefinition> columnDefinitions =
columns.stream().map(col -> {
+ DataType dataType =
DataType.fromCatalogType(col.getType());
+ return new ColumnDefinition(col.getName(), dataType,
col.isAllowNull(), col.getComment());
+ }).collect(Collectors.toList());
+
+ // Create DistributionDescriptor
+ DistributionDescriptor distribution = new
DistributionDescriptor(
+ true, // isHash
+ true, // isAutoBucket
+ FeConstants.default_bucket_num,
+ primaryKeys
+ );
+
+ // Create CreateTableInfo
+ CreateTableInfo createtblInfo = new CreateTableInfo(
+ true, // ifNotExists
+ false, // isExternal
+ false, // isTemp
+ InternalCatalog.INTERNAL_CATALOG_NAME, // ctlName
+ targetDb, // dbName
+ targetTableName, // tableName
+ columnDefinitions, // columns
+ ImmutableList.of(), // indexes
+ "olap", // engineName
+ KeysType.UNIQUE_KEYS, // keysType
+ primaryKeys, // keys
+ "", // comment
+ PartitionTableInfo.EMPTY, // partitionTableInfo
+ distribution, // distribution
+ ImmutableList.of(), // rollups
+ new HashMap<>(tableCreateProperties), // properties
+ ImmutableMap.of(), // extProperties
+ ImmutableList.of() // clusterKeyColumnNames
+ );
+ CreateTableCommand createtblCmd = new
CreateTableCommand(Optional.empty(), createtblInfo);
+ // Key: source (PG/MySQL) table name; Value: command that
creates the Doris target table
+ createtblCmds.put(table, createtblCmd);
+ }
+ if (createtblCmds.isEmpty()) {
+ throw new JobException("Can not found match table in database
" + database);
+ }
- if (!noPrimaryKeyTables.isEmpty()) {
- throw new JobException("The following tables do not have primary
key defined: "
- + String.join(", ", noPrimaryKeyTables));
+ if (!noPrimaryKeyTables.isEmpty()) {
+ throw new JobException("The following tables do not have
primary key defined: "
+ + String.join(", ", noPrimaryKeyTables));
+ }
+ return createtblCmds;
+ } finally {
+ jdbcClient.closeClient();
}
- return createtblCmds;
}
public static List<Column> getColumns(JdbcClient jdbcClient,
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerManager.java
b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerManager.java
index 3cfa387efa7..1268341600d 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerManager.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerManager.java
@@ -61,8 +61,9 @@ public class AccessControllerManager {
private Auth auth;
// Default access controller instance used for handling cases where no
specific controller is specified
private CatalogAccessController defaultAccessController;
- // Map that stores the mapping between catalogs and their corresponding
access controllers
- private Map<String, CatalogAccessController> ctlToCtlAccessController =
Maps.newConcurrentMap();
+ // A catalog name can be reused after DROP. Keep the catalog id next to
the controller so cleanup from
+ // an old catalog generation can never remove or close the replacement
generation's controller.
+ private Map<String, CatalogAccessControllerEntry> ctlToCtlAccessController
= Maps.newConcurrentMap();
// Cache of loaded access controller factories for quick creation of new
access controllers
private ConcurrentHashMap<String, AccessControllerFactory>
accessControllerFactoriesCache
= new ConcurrentHashMap<>();
@@ -74,7 +75,24 @@ public class AccessControllerManager {
loadAccessControllerPlugins();
String accessControllerName = Config.access_controller_type;
this.defaultAccessController =
loadAccessControllerOrThrow(accessControllerName);
- ctlToCtlAccessController.put(InternalCatalog.INTERNAL_CATALOG_NAME,
defaultAccessController);
+ ctlToCtlAccessController.put(InternalCatalog.INTERNAL_CATALOG_NAME,
+ new CatalogAccessControllerEntry(
+ InternalCatalog.INTERNAL_CATALOG_ID,
defaultAccessController, false));
+ }
+
+ private static final class CatalogAccessControllerEntry {
+ private final long catalogId;
+ private final CatalogAccessController accessController;
+ // The default controller is shared with the internal catalog. Catalog
aliases must detach it but never
+ // close it when an external catalog is reset or dropped.
+ private final boolean owned;
+
+ private CatalogAccessControllerEntry(
+ long catalogId, CatalogAccessController accessController,
boolean owned) {
+ this.catalogId = catalogId;
+ this.accessController = accessController;
+ this.owned = owned;
+ }
}
private CatalogAccessController loadAccessControllerOrThrow(String
accessControllerName) {
@@ -116,26 +134,66 @@ public class AccessControllerManager {
}
public CatalogAccessController getAccessControllerOrDefault(String ctl) {
- CatalogAccessController catalogAccessController =
ctlToCtlAccessController.get(ctl);
- if (catalogAccessController != null) {
- return catalogAccessController;
+ if (InternalCatalog.INTERNAL_CATALOG_NAME.equals(ctl)) {
+ return defaultAccessController;
}
CatalogIf catalog =
Env.getCurrentEnv().getCatalogMgr().getCatalog(ctl);
if (catalog != null && catalog instanceof ExternalCatalog) {
+ CatalogAccessControllerEntry entry =
ctlToCtlAccessController.get(ctl);
+ if (entry != null && entry.catalogId == catalog.getId()) {
+ return entry.accessController;
+ }
lazyLoadCtlAccessController((ExternalCatalog) catalog);
- return ctlToCtlAccessController.get(ctl);
+ entry = ctlToCtlAccessController.get(ctl);
+ if (entry != null && entry.catalogId == catalog.getId()) {
+ return entry.accessController;
+ }
}
return defaultAccessController;
}
- private synchronized void lazyLoadCtlAccessController(ExternalCatalog
catalog) {
- if (ctlToCtlAccessController.containsKey(catalog.getName())) {
- return;
+ private void lazyLoadCtlAccessController(ExternalCatalog catalog) {
+ CatalogAccessControllerEntry staleEntry = null;
+ synchronized (this) {
+ if (!isCurrentCatalog(catalog)) {
+ return;
+ }
+ CatalogAccessControllerEntry entry =
ctlToCtlAccessController.get(catalog.getName());
+ if (entry != null && entry.catalogId == catalog.getId()) {
+ return;
+ }
+ if (entry != null &&
ctlToCtlAccessController.remove(catalog.getName(), entry)) {
+ staleEntry = entry;
+ }
}
+ closeEntry(catalog.getName(), staleEntry);
+
catalog.initAccessController(false);
- if (!ctlToCtlAccessController.containsKey(catalog.getName())) {
- ctlToCtlAccessController.put(catalog.getName(),
defaultAccessController);
+
+ CatalogAccessControllerEntry displaced = null;
+ boolean stillCurrent;
+ synchronized (this) {
+ stillCurrent = isCurrentCatalog(catalog);
+ if (stillCurrent) {
+ CatalogAccessControllerEntry entry =
ctlToCtlAccessController.get(catalog.getName());
+ if (entry == null || entry.catalogId != catalog.getId()) {
+ displaced = ctlToCtlAccessController.put(catalog.getName(),
+ new CatalogAccessControllerEntry(catalog.getId(),
defaultAccessController, false));
+ }
+ }
+ }
+ closeEntry(catalog.getName(), displaced);
+ if (!stillCurrent) {
+ // A DROP can complete while initAccessController() is
constructing the plugin. The custom
+ // publication path performs the same post-publication check; this
also covers the fallback path.
+ removeAccessController(catalog.getName(), catalog.getId());
+ return;
+ }
+ // If DROP won immediately after the synchronized publication, its
onClose() removes this id. If DROP
+ // already completed before publication, this final identity check
removes the orphan ourselves.
+ if (!isCurrentCatalog(catalog)) {
+ removeAccessController(catalog.getName(), catalog.getId());
}
}
@@ -143,17 +201,45 @@ public class AccessControllerManager {
return ctlToCtlAccessController.containsKey(ctl);
}
- public void createAccessController(String ctl, String acFactoryClassName,
Map<String, String> prop,
+ public void createAccessController(ExternalCatalog catalog, String
acFactoryClassName, Map<String, String> prop,
boolean isDryRun) {
String pluginIdentifier =
getPluginIdentifierForAccessController(acFactoryClassName);
CatalogAccessController accessController =
accessControllerFactoriesCache.get(pluginIdentifier)
.createAccessController(prop);
- if (!isDryRun) {
- ctlToCtlAccessController.put(ctl, accessController);
- LOG.info("create access controller {} for catalog {}",
acFactoryClassName, ctl);
+ if (isDryRun) {
+ closeAccessController(catalog.getName(), accessController);
+ return;
+ }
+
+ CatalogAccessControllerEntry displaced = null;
+ boolean installed = false;
+ synchronized (this) {
+ if (isCurrentCatalog(catalog)) {
+ CatalogAccessControllerEntry current =
ctlToCtlAccessController.get(catalog.getName());
+ if (current == null || current.catalogId != catalog.getId()) {
+ displaced = ctlToCtlAccessController.put(catalog.getName(),
+ new CatalogAccessControllerEntry(catalog.getId(),
accessController, true));
+ installed = true;
+ }
+ }
+ }
+ closeEntry(catalog.getName(), displaced);
+ if (!installed) {
+ closeAccessController(catalog.getName(), accessController);
+ return;
+ }
+ LOG.info("create access controller {} for catalog {}:{}",
+ acFactoryClassName, catalog.getName(), catalog.getId());
+ if (!isCurrentCatalog(catalog)) {
+ removeAccessController(catalog.getName(), catalog.getId());
}
}
+ private boolean isCurrentCatalog(ExternalCatalog catalog) {
+ CatalogIf currentCatalog =
Env.getCurrentEnv().getCatalogMgr().getCatalog(catalog.getName());
+ return currentCatalog == catalog && currentCatalog.getId() ==
catalog.getId();
+ }
+
private String getPluginIdentifierForAccessController(String acClassName) {
String pluginIdentifier = null;
if (accessControllerClassNameMapping.containsKey(acClassName)) {
@@ -168,14 +254,41 @@ public class AccessControllerManager {
return pluginIdentifier;
}
- public void removeAccessController(String ctl) {
+ public void removeAccessController(String ctl, long catalogId) {
+ detachAccessController(ctl, catalogId).run();
+ }
+
+ /**
+ * Atomically detach the controller owned by one catalog generation. The
returned cleanup can be executed
+ * after the caller releases CatalogMgr's global lock, so a slow plugin
close never blocks unrelated DDL.
+ */
+ public Runnable detachAccessController(String ctl, long catalogId) {
if (StringUtils.isBlank(ctl)) {
+ return () -> { };
+ }
+ CatalogAccessControllerEntry entry = ctlToCtlAccessController.get(ctl);
+ if (entry == null || entry.catalogId != catalogId ||
!ctlToCtlAccessController.remove(ctl, entry)) {
+ return () -> { };
+ }
+ LOG.info("detach access controller for catalog {}:{}", ctl, catalogId);
+ return () -> closeEntry(ctl, entry);
+ }
+
+ private void closeEntry(String ctl, CatalogAccessControllerEntry entry) {
+ if (entry == null || !entry.owned || entry.accessController ==
defaultAccessController) {
return;
}
- if (ctlToCtlAccessController.containsKey(ctl)) {
- ctlToCtlAccessController.remove(ctl);
+ closeAccessController(ctl, entry.accessController);
+ }
+
+ private void closeAccessController(String ctl, CatalogAccessController
accessController) {
+ try {
+ accessController.close();
+ } catch (Throwable e) {
+ // Access-controller plugins are external code. A faulty cleanup
must not prevent the catalog
+ // lifecycle from releasing its own resources.
+ LOG.warn("Failed to close access controller for catalog {}", ctl,
e);
}
- LOG.info("remove access controller for catalog {}", ctl);
}
public Auth getAuth() {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/CatalogAccessController.java
b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/CatalogAccessController.java
index df7605256dd..8e548b7463f 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/CatalogAccessController.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/CatalogAccessController.java
@@ -26,6 +26,9 @@ import java.util.Optional;
import java.util.Set;
public interface CatalogAccessController {
+ default void close() {
+ }
+
// ==== Catalog ====
default boolean checkCtlPriv(boolean hasGlobal, UserIdentity currentUser,
String ctl, PrivPredicate wanted) {
if (hasGlobal) {
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAuditLogFlusherTest.java
b/fe/fe-core/src/test/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAuditLogFlusherTest.java
new file mode 100644
index 00000000000..92e9440aaf3
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAuditLogFlusherTest.java
@@ -0,0 +1,139 @@
+// 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.catalog.authorizer.ranger.hive;
+
+import org.apache.ranger.audit.model.AuthzAuditEvent;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class RangerHiveAuditLogFlusherTest {
+
+ @Test
+ public void testRunFlushesOnceAndReturns() {
+ RangerHiveAuditHandler auditHandler =
Mockito.mock(RangerHiveAuditHandler.class);
+
+ new RangerHiveAuditLogFlusher(auditHandler).run();
+
+ Mockito.verify(auditHandler).flushAudit();
+ }
+
+ @Test
+ public void testRunContainsProviderFailureSoLaterTicksContinue() {
+ RangerHiveAuditHandler auditHandler =
Mockito.mock(RangerHiveAuditHandler.class);
+ Mockito.doThrow(new RuntimeException("provider unavailable"))
+ .doNothing().when(auditHandler).flushAudit();
+ RangerHiveAuditLogFlusher flusher = new
RangerHiveAuditLogFlusher(auditHandler);
+
+ flusher.run();
+ flusher.run();
+
+ Mockito.verify(auditHandler, Mockito.times(2)).flushAudit();
+ }
+
+ @Test
+ public void testProducerCanEnqueueWhileProviderDeliveryIsBlocked() throws
Exception {
+ RecordingAuditHandler auditHandler = new RecordingAuditHandler(false,
true);
+ AuthzAuditEvent first = allowedEvent();
+ AuthzAuditEvent second = allowedEvent();
+ auditHandler.addAuthzAuditEvent(first);
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+
+ try {
+ Future<?> flush = executor.submit(auditHandler::flushAudit);
+ Assert.assertTrue(auditHandler.deliveryStarted.await(10,
TimeUnit.SECONDS));
+
+ Future<?> producer = executor.submit(() ->
auditHandler.addAuthzAuditEvent(second));
+ producer.get(2, TimeUnit.SECONDS);
+
+ auditHandler.allowDelivery.countDown();
+ flush.get(10, TimeUnit.SECONDS);
+ auditHandler.flushAudit();
+
+ Assert.assertEquals(List.of(first, second),
auditHandler.delivered);
+ } finally {
+ auditHandler.allowDelivery.countDown();
+ executor.shutdownNow();
+ }
+ }
+
+ @Test
+ public void testFailedDeliveryIsRetriedBeforeLaterEvents() {
+ RecordingAuditHandler auditHandler = new RecordingAuditHandler(true,
false);
+ AuthzAuditEvent first = allowedEvent();
+ AuthzAuditEvent second = allowedEvent();
+ auditHandler.addAuthzAuditEvent(first);
+
+ Assert.assertThrows(RuntimeException.class, auditHandler::flushAudit);
+ Assert.assertEquals(1,
auditHandler.getPendingAuditEventCountForTest());
+
+ auditHandler.addAuthzAuditEvent(second);
+ auditHandler.flushAudit();
+
+ Assert.assertEquals(List.of(first, second), auditHandler.delivered);
+ Assert.assertEquals(0,
auditHandler.getPendingAuditEventCountForTest());
+ }
+
+ private static AuthzAuditEvent allowedEvent() {
+ AuthzAuditEvent event = new AuthzAuditEvent();
+ event.setAccessResult((short) 1);
+ return event;
+ }
+
+ private static class RecordingAuditHandler extends RangerHiveAuditHandler {
+ private final AtomicBoolean failNext;
+ private final boolean blockFirstDelivery;
+ private final AtomicBoolean firstDelivery = new AtomicBoolean(true);
+ private final CountDownLatch deliveryStarted = new CountDownLatch(1);
+ private final CountDownLatch allowDelivery = new CountDownLatch(1);
+ private final List<AuthzAuditEvent> delivered = new
CopyOnWriteArrayList<>();
+
+ RecordingAuditHandler(boolean failFirstDelivery, boolean
blockFirstDelivery) {
+ this.failNext = new AtomicBoolean(failFirstDelivery);
+ this.blockFirstDelivery = blockFirstDelivery;
+ }
+
+ @Override
+ protected void logAuditEvent(AuthzAuditEvent auditEvent) {
+ if (blockFirstDelivery && firstDelivery.compareAndSet(true,
false)) {
+ deliveryStarted.countDown();
+ try {
+ if (!allowDelivery.await(10, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("timed out waiting to
release audit provider");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
+ }
+ }
+ if (failNext.compareAndSet(true, false)) {
+ throw new RuntimeException("provider unavailable");
+ }
+ delivered.add(auditEvent);
+ }
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/datasource/CatalogFactoryTest.java
b/fe/fe-core/src/test/java/org/apache/doris/datasource/CatalogFactoryTest.java
new file mode 100644
index 00000000000..c03f9c4ade7
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/datasource/CatalogFactoryTest.java
@@ -0,0 +1,90 @@
+// 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.datasource;
+
+import org.apache.doris.common.DdlException;
+import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.connector.DefaultConnectorContext;
+import org.apache.doris.connector.spi.Connector;
+import org.apache.doris.datasource.plugin.PluginDrivenExternalCatalog;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.HashMap;
+
+public class CatalogFactoryTest {
+
+ @Test
+ public void testCloseCatalogWhenCreateValidationFails() throws Exception {
+ Connector connector = Mockito.mock(Connector.class);
+ PluginDrivenExternalCatalog catalog = Mockito.spy(new
PluginDrivenExternalCatalog(
+ 1L, "failed_catalog", null, new HashMap<>(), "", connector));
+ Mockito.doThrow(new DdlException("validation
failed")).when(catalog).checkWhenCreating();
+
+ DdlException exception = Assert.assertThrows(
+ DdlException.class, () ->
CatalogFactory.finishCatalogCreation(catalog, false));
+
+ Assert.assertTrue(exception.getMessage().endsWith("validation
failed"));
+ Mockito.verify(connector).close();
+ }
+
+ @Test
+ public void testPreserveValidationFailureWhenCleanupFails() throws
Exception {
+ Connector connector = Mockito.mock(Connector.class);
+ Mockito.doThrow(new RuntimeException("cleanup
failed")).when(connector).close();
+ PluginDrivenExternalCatalog catalog = new PluginDrivenExternalCatalog(
+ 2L, "failed_catalog", null, new HashMap<>(), "", connector) {
+ @Override
+ public void checkWhenCreating() throws DdlException {
+ throw new DdlException("primary validation failure");
+ }
+ };
+
+ DdlException exception = Assert.assertThrows(
+ DdlException.class, () ->
CatalogFactory.finishCatalogCreation(catalog, false));
+
+ Assert.assertTrue(exception.getMessage().endsWith("primary validation
failure"));
+ }
+
+ @Test
+ public void
testRuntimeConnectorFailureDoesNotSkipContextCleanupOrRetryConnector() throws
Exception {
+ Connector connector = Mockito.mock(Connector.class);
+ DefaultConnectorContext connectorContext =
Mockito.mock(DefaultConnectorContext.class);
+ Mockito.doThrow(new RuntimeException("connector cleanup
failed")).when(connector).close();
+ TestablePluginCatalog catalog = new TestablePluginCatalog(connector);
+ Deencapsulation.setField(catalog, "connectorContext",
connectorContext);
+
+ catalog.closeResourcesForTest();
+ catalog.closeResourcesForTest();
+
+ Mockito.verify(connector).close();
+ Mockito.verify(connectorContext).close();
+ }
+
+ private static class TestablePluginCatalog extends
PluginDrivenExternalCatalog {
+ TestablePluginCatalog(Connector connector) {
+ super(3L, "registered_catalog", null, new HashMap<>(), "",
connector);
+ }
+
+ void closeResourcesForTest() {
+ closeResources();
+ }
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/datasource/CatalogMgrTest.java
b/fe/fe-core/src/test/java/org/apache/doris/datasource/CatalogMgrTest.java
index fb43b3d99c8..e16cab34a80 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/datasource/CatalogMgrTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/datasource/CatalogMgrTest.java
@@ -113,6 +113,66 @@ public class CatalogMgrTest {
}
}
+ @Test
+ void testAlterControllerCleanupRunsOutsideCatalogWriteLock() throws
Exception {
+ CatalogMgr catalogMgr = new CatalogMgr();
+ ExternalCatalog blockingCatalog = Mockito.mock(ExternalCatalog.class);
+ ExternalCatalog otherCatalog = Mockito.mock(ExternalCatalog.class);
+ Mockito.when(blockingCatalog.getId()).thenReturn(44L);
+ Mockito.when(otherCatalog.getId()).thenReturn(45L);
+
Mockito.when(blockingCatalog.validatePropertiesBeforeUpdate(Mockito.anyMap(),
Mockito.anyMap()))
+ .thenReturn(true);
+
Mockito.when(otherCatalog.validatePropertiesBeforeUpdate(Mockito.anyMap(),
Mockito.anyMap()))
+ .thenReturn(true);
+ addCatalog(catalogMgr, blockingCatalog);
+ addCatalog(catalogMgr, otherCatalog);
+
+ CountDownLatch cleanupStarted = new CountDownLatch(1);
+ CountDownLatch allowCleanup = new CountDownLatch(1);
+
Mockito.when(blockingCatalog.modifyCatalogPropsWithDeferredAccessControllerCleanup(Mockito.anyMap()))
+ .thenReturn(() -> {
+ cleanupStarted.countDown();
+ try {
+ if (!allowCleanup.await(10, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("timed out waiting
to release controller cleanup");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
+ }
+ });
+
Mockito.when(otherCatalog.modifyCatalogPropsWithDeferredAccessControllerCleanup(Mockito.anyMap()))
+ .thenReturn(() -> { });
+
+ CatalogLog blockingLog = new CatalogLog();
+ blockingLog.setCatalogId(44L);
+ blockingLog.setNewProps(ImmutableMap.of("k", "v1"));
+ CatalogLog otherLog = new CatalogLog();
+ otherLog.setCatalogId(45L);
+ otherLog.setNewProps(ImmutableMap.of("k", "v2"));
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+
+ try {
+ Future<?> blockingAlter = executor.submit(() -> {
+ catalogMgr.replayAlterCatalogProps(blockingLog,
Collections.emptyMap(), false);
+ return null;
+ });
+ Assertions.assertTrue(cleanupStarted.await(10, TimeUnit.SECONDS));
+
+ Future<?> unrelatedAlter = executor.submit(() -> {
+ catalogMgr.replayAlterCatalogProps(otherLog,
Collections.emptyMap(), false);
+ return null;
+ });
+ unrelatedAlter.get(2, TimeUnit.SECONDS);
+
+ allowCleanup.countDown();
+ blockingAlter.get(10, TimeUnit.SECONDS);
+ } finally {
+ allowCleanup.countDown();
+ executor.shutdownNow();
+ }
+ }
+
private static class LatchingValidationCatalog extends ExternalCatalog {
private final CountDownLatch validationStarted = new CountDownLatch(1);
private final CountDownLatch initializationReadProperties = new
CountDownLatch(1);
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalCatalogDeadlockTest.java
b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalCatalogDeadlockTest.java
index c6a9ce8841a..c9a46b4c1dd 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalCatalogDeadlockTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalCatalogDeadlockTest.java
@@ -32,12 +32,15 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class ExternalCatalogDeadlockTest {
+ private static final long DEADLOCK_DETECTION_TIMEOUT_SECONDS = 5;
+ private static final long LOADER_PERMISSION_TIMEOUT_SECONDS = 30;
@Test
public void testResetToUninitializedShouldNotDeadlockWithCacheLoader()
throws Exception {
DeadlockCatalog catalog = new DeadlockCatalog();
CountDownLatch loaderEntered = new CountDownLatch(1);
CountDownLatch allowLoaderToTouchCatalog = new CountDownLatch(1);
+ CountDownLatch refreshReady = new CountDownLatch(1);
AtomicReference<Throwable> backgroundFailure = new AtomicReference<>();
// The loader holds Caffeine's per-key lock before it calls back into
the catalog.
@@ -53,7 +56,7 @@ public class ExternalCatalogDeadlockTest {
"deadlock-cache-loader");
queryThread.setDaemon(true);
queryThread.start();
- Assertions.assertTrue(loaderEntered.await(5, TimeUnit.SECONDS));
+
Assertions.assertTrue(loaderEntered.await(DEADLOCK_DETECTION_TIMEOUT_SECONDS,
TimeUnit.SECONDS));
Thread refreshThread = new Thread(
() -> runQuietly(backgroundFailure, () -> {
@@ -62,11 +65,13 @@ public class ExternalCatalogDeadlockTest {
allowLoaderToTouchCatalog.countDown();
cache.invalidate("deadlock-key");
});
+ refreshReady.countDown();
catalog.resetToUninitialized(true);
}),
"deadlock-catalog-refresh");
refreshThread.setDaemon(true);
refreshThread.start();
+
Assertions.assertTrue(refreshReady.await(DEADLOCK_DETECTION_TIMEOUT_SECONDS,
TimeUnit.SECONDS));
assertNoDeadlock(queryThread, refreshThread, backgroundFailure);
}
@@ -74,8 +79,8 @@ public class ExternalCatalogDeadlockTest {
private static void assertNoDeadlock(Thread queryThread, Thread
refreshThread,
AtomicReference<Throwable> backgroundFailure) throws Exception {
long[] deadlockedThreads = waitForDeadlock(queryThread, refreshThread);
- queryThread.join(TimeUnit.SECONDS.toMillis(5));
- refreshThread.join(TimeUnit.SECONDS.toMillis(5));
+
queryThread.join(TimeUnit.SECONDS.toMillis(DEADLOCK_DETECTION_TIMEOUT_SECONDS));
+
refreshThread.join(TimeUnit.SECONDS.toMillis(DEADLOCK_DETECTION_TIMEOUT_SECONDS));
Assertions.assertNull(backgroundFailure.get(), "unexpected background
failure: " + backgroundFailure.get());
Assertions.assertNull(deadlockedThreads,
String.format("detected deadlock between threads %s and %s",
@@ -85,7 +90,7 @@ public class ExternalCatalogDeadlockTest {
}
private static void awaitLatch(CountDownLatch latch) throws
InterruptedException {
- Assertions.assertTrue(latch.await(5, TimeUnit.SECONDS));
+ Assertions.assertTrue(latch.await(LOADER_PERMISSION_TIMEOUT_SECONDS,
TimeUnit.SECONDS));
}
private static void runQuietly(AtomicReference<Throwable> failure,
ThrowingRunnable task) {
@@ -98,7 +103,7 @@ public class ExternalCatalogDeadlockTest {
private static long[] waitForDeadlock(Thread queryThread, Thread
refreshThread) throws InterruptedException {
ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean();
- for (int i = 0; i < 100; i++) {
+ for (int i = 0; i < DEADLOCK_DETECTION_TIMEOUT_SECONDS * 20; i++) {
long[] deadlockedThreads = threadMxBean.findDeadlockedThreads();
if (deadlockedThreads != null
&& contains(deadlockedThreads, queryThread.getId())
@@ -132,7 +137,7 @@ public class ExternalCatalogDeadlockTest {
}
@Override
- public void onClose() {
+ protected void closeResources() {
}
@Override
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/datasource/jdbc/client/JdbcOceanBaseClientTest.java
b/fe/fe-core/src/test/java/org/apache/doris/datasource/jdbc/client/JdbcOceanBaseClientTest.java
new file mode 100644
index 00000000000..b2632b8fc10
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/datasource/jdbc/client/JdbcOceanBaseClientTest.java
@@ -0,0 +1,105 @@
+// 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.datasource.jdbc.client;
+
+import com.zaxxer.hikari.HikariDataSource;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.MockedConstruction;
+import org.mockito.Mockito;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.Statement;
+
+public class JdbcOceanBaseClientTest {
+ private Connection connection;
+ private Statement statement;
+ private ResultSet resultSet;
+
+ @Before
+ public void setUp() throws Exception {
+ connection = Mockito.mock(Connection.class);
+ statement = Mockito.mock(Statement.class);
+ resultSet = Mockito.mock(ResultSet.class);
+ Mockito.when(connection.createStatement()).thenReturn(statement);
+ Mockito.when(statement.executeQuery("SHOW VARIABLES LIKE
'ob_compatibility_mode'")).thenReturn(resultSet);
+ }
+
+ @Test
+ public void testCloseTemporaryDataSourceAfterCreatingClient() throws
Exception {
+ Mockito.when(resultSet.next()).thenReturn(true);
+ Mockito.when(resultSet.getString(2)).thenReturn("MYSQL");
+
+ try (MockedConstruction<HikariDataSource> mockedDataSources =
mockDataSources()) {
+ JdbcOceanBaseClient oceanBaseClient = new
JdbcOceanBaseClient(createConfig());
+ JdbcClient client = oceanBaseClient.createClient(createConfig());
+
+ Assert.assertTrue(client instanceof JdbcMySQLClient);
+ Assert.assertEquals(2, mockedDataSources.constructed().size());
+ HikariDataSource temporaryDataSource =
mockedDataSources.constructed().get(0);
+ HikariDataSource clientDataSource =
mockedDataSources.constructed().get(1);
+ assertTemporaryResourcesClosed(temporaryDataSource);
+ Mockito.verify(clientDataSource, Mockito.never()).close();
+
+ client.closeClient();
+ Mockito.verify(clientDataSource).close();
+ }
+ }
+
+ @Test
+ public void testCloseTemporaryDataSourceWhenCompatibilityModeIsMissing()
throws Exception {
+ Mockito.when(resultSet.next()).thenReturn(false);
+
+ try (MockedConstruction<HikariDataSource> mockedDataSources =
mockDataSources()) {
+ JdbcOceanBaseClient oceanBaseClient = new
JdbcOceanBaseClient(createConfig());
+
+ JdbcClientException exception = Assert.assertThrows(
+ JdbcClientException.class, () ->
oceanBaseClient.createClient(createConfig()));
+
+ Assert.assertEquals("Failed to determine OceanBase compatibility
mode", exception.getMessage());
+ Assert.assertEquals(1, mockedDataSources.constructed().size());
+
assertTemporaryResourcesClosed(mockedDataSources.constructed().get(0));
+ }
+ }
+
+ private MockedConstruction<HikariDataSource> mockDataSources() {
+ return Mockito.mockConstruction(HikariDataSource.class, (mock,
context) ->
+ Mockito.when(mock.getConnection()).thenReturn(connection));
+ }
+
+ private JdbcClientConfig createConfig() {
+ return new JdbcClientConfig()
+ .setCatalog("oceanbase_catalog")
+ .setUser("user")
+ .setPassword("password")
+ .setJdbcUrl("jdbc:oceanbase://localhost:2881/test")
+ .setDriverUrl("file:///tmp/oceanbase-jdbc.jar")
+ .setDriverClass("com.oceanbase.jdbc.Driver");
+ }
+
+ private void assertTemporaryResourcesClosed(HikariDataSource
temporaryDataSource) throws Exception {
+ InOrder inOrder = Mockito.inOrder(resultSet, statement, connection,
temporaryDataSource);
+ inOrder.verify(resultSet).close();
+ inOrder.verify(statement).close();
+ inOrder.verify(connection).close();
+ inOrder.verify(temporaryDataSource).close();
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/job/util/StreamingJobUtilsTest.java
b/fe/fe-core/src/test/java/org/apache/doris/job/util/StreamingJobUtilsTest.java
index 93c19074cd1..b2e2b0a56be 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/job/util/StreamingJobUtilsTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/job/util/StreamingJobUtilsTest.java
@@ -23,12 +23,14 @@ import org.apache.doris.catalog.ScalarType;
import org.apache.doris.datasource.jdbc.client.JdbcClient;
import org.apache.doris.job.cdc.DataSourceConfigKeys;
import org.apache.doris.job.common.DataSourceType;
+import org.apache.doris.job.exception.JobException;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
+import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@@ -250,4 +252,22 @@ public class StreamingJobUtilsTest {
Assert.assertEquals("test_db",
StreamingJobUtils.getRemoteDbName(DataSourceType.OCEANBASE,
properties));
}
+
+ @Test
+ public void testGenerateCreateTableCmdsClosesJdbcClientOnFailure() {
+ Map<String, String> properties = new HashMap<>();
+ try (MockedStatic<StreamingJobUtils> utils =
Mockito.mockStatic(StreamingJobUtils.class,
+ Mockito.CALLS_REAL_METHODS)) {
+ utils.when(() ->
StreamingJobUtils.getJdbcClient(DataSourceType.OCEANBASE, properties))
+ .thenReturn(jdbcClient);
+ utils.when(() ->
StreamingJobUtils.getRemoteDbName(DataSourceType.OCEANBASE, properties))
+ .thenReturn("test_db");
+
Mockito.when(jdbcClient.getTablesNameList("test_db")).thenReturn(new
ArrayList<>());
+
+ Assert.assertThrows(JobException.class, () ->
StreamingJobUtils.generateCreateTableCmds(
+ "target_db", DataSourceType.OCEANBASE, properties, new
HashMap<>()));
+
+ Mockito.verify(jdbcClient).closeClient();
+ }
+ }
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/mysql/privilege/AccessControllerManagerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/mysql/privilege/AccessControllerManagerTest.java
index bebe5e98ef3..a6b2514b43a 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/mysql/privilege/AccessControllerManagerTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/mysql/privilege/AccessControllerManagerTest.java
@@ -23,6 +23,7 @@ import org.apache.doris.common.Config;
import org.apache.doris.common.jmockit.Deencapsulation;
import org.apache.doris.datasource.CatalogIf;
import org.apache.doris.datasource.CatalogMgr;
+import org.apache.doris.datasource.ExternalCatalog;
import com.google.common.collect.ImmutableMap;
import org.junit.After;
@@ -32,6 +33,9 @@ import org.junit.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+
public class AccessControllerManagerTest {
private boolean originalSkipCatalogPrivCheck;
@@ -193,6 +197,164 @@ public class AccessControllerManagerTest {
}
}
+ @Test
+ public void testDryRunClosesTemporaryAccessController() {
+ CatalogAccessController defaultAccessController =
Mockito.mock(CatalogAccessController.class);
+ CatalogAccessController temporaryAccessController =
Mockito.mock(CatalogAccessController.class);
+ AccessControllerFactory factory =
Mockito.mock(AccessControllerFactory.class);
+ AccessControllerManager accessControllerManager =
createAccessControllerManager(defaultAccessController);
+ ConcurrentHashMap<String, AccessControllerFactory> factories =
+ Deencapsulation.getField(accessControllerManager,
"accessControllerFactoriesCache");
+ factories.put("test-controller", factory);
+
Mockito.when(factory.createAccessController(ImmutableMap.of())).thenReturn(temporaryAccessController);
+ ExternalCatalog catalog = mockCatalog("test_catalog", 1L);
+
+ accessControllerManager.createAccessController(
+ catalog, "test-controller", ImmutableMap.of(), true);
+
+ Mockito.verify(temporaryAccessController).close();
+
Assert.assertFalse(accessControllerManager.checkIfAccessControllerExist("test_catalog"));
+ }
+
+ @Test
+ public void testRemoveClosesRegisteredAccessController() {
+ CatalogAccessController defaultAccessController =
Mockito.mock(CatalogAccessController.class);
+ CatalogAccessController registeredAccessController =
Mockito.mock(CatalogAccessController.class);
+ AccessControllerFactory factory =
Mockito.mock(AccessControllerFactory.class);
+ AccessControllerManager accessControllerManager =
createAccessControllerManager(defaultAccessController);
+ ConcurrentHashMap<String, AccessControllerFactory> factories =
+ Deencapsulation.getField(accessControllerManager,
"accessControllerFactoriesCache");
+ factories.put("test-controller", factory);
+
Mockito.when(factory.createAccessController(ImmutableMap.of())).thenReturn(registeredAccessController);
+ ExternalCatalog catalog = mockCatalog("test_catalog", 1L);
+
+ withCurrentCatalog(catalog, () ->
accessControllerManager.createAccessController(
+ catalog, "test-controller", ImmutableMap.of(), false));
+ accessControllerManager.removeAccessController("test_catalog",
catalog.getId());
+
+ Mockito.verify(registeredAccessController).close();
+
Assert.assertFalse(accessControllerManager.checkIfAccessControllerExist("test_catalog"));
+ }
+
+ @Test
+ public void testRemoveContinuesWhenAccessControllerCloseFails() {
+ CatalogAccessController defaultAccessController =
Mockito.mock(CatalogAccessController.class);
+ CatalogAccessController registeredAccessController =
Mockito.mock(CatalogAccessController.class);
+ AccessControllerFactory factory =
Mockito.mock(AccessControllerFactory.class);
+ AccessControllerManager accessControllerManager =
createAccessControllerManager(defaultAccessController);
+ ConcurrentHashMap<String, AccessControllerFactory> factories =
+ Deencapsulation.getField(accessControllerManager,
"accessControllerFactoriesCache");
+ factories.put("test-controller", factory);
+
Mockito.when(factory.createAccessController(ImmutableMap.of())).thenReturn(registeredAccessController);
+ Mockito.doThrow(new RuntimeException("plugin cleanup
failure")).when(registeredAccessController).close();
+ ExternalCatalog catalog = mockCatalog("test_catalog", 1L);
+
+ withCurrentCatalog(catalog, () ->
accessControllerManager.createAccessController(
+ catalog, "test-controller", ImmutableMap.of(), false));
+ accessControllerManager.removeAccessController("test_catalog",
catalog.getId());
+
+ Mockito.verify(registeredAccessController).close();
+
Assert.assertFalse(accessControllerManager.checkIfAccessControllerExist("test_catalog"));
+ }
+
+ @Test
+ public void testOldGenerationCannotRemoveReplacementController() {
+ CatalogAccessController defaultAccessController =
Mockito.mock(CatalogAccessController.class);
+ CatalogAccessController oldController =
Mockito.mock(CatalogAccessController.class);
+ CatalogAccessController newController =
Mockito.mock(CatalogAccessController.class);
+ AccessControllerFactory factory =
Mockito.mock(AccessControllerFactory.class);
+ AccessControllerManager manager =
createAccessControllerManager(defaultAccessController);
+ ConcurrentHashMap<String, AccessControllerFactory> factories =
+ Deencapsulation.getField(manager,
"accessControllerFactoriesCache");
+ factories.put("test-controller", factory);
+
Mockito.when(factory.createAccessController(ImmutableMap.of())).thenReturn(oldController,
newController);
+ ExternalCatalog oldCatalog = mockCatalog("same_name", 10L);
+ ExternalCatalog newCatalog = mockCatalog("same_name", 11L);
+
+ CatalogMgr catalogMgr = Mockito.mock(CatalogMgr.class);
+ AtomicReference<CatalogIf> currentCatalog = new
AtomicReference<>(oldCatalog);
+ try (MockedStatic<Env> mockedEnv = Mockito.mockStatic(Env.class)) {
+ Env env = Mockito.mock(Env.class);
+ mockedEnv.when(Env::getCurrentEnv).thenReturn(env);
+ Mockito.when(env.getCatalogMgr()).thenReturn(catalogMgr);
+
Mockito.when(catalogMgr.getCatalog("same_name")).thenAnswer(invocation ->
currentCatalog.get());
+
+ manager.createAccessController(oldCatalog, "test-controller",
ImmutableMap.of(), false);
+ currentCatalog.set(newCatalog);
+ manager.createAccessController(newCatalog, "test-controller",
ImmutableMap.of(), false);
+ manager.removeAccessController("same_name", oldCatalog.getId());
+
+ Assert.assertSame(newController,
manager.getAccessControllerOrDefault("same_name"));
+ }
+
+ Mockito.verify(oldController).close();
+ Mockito.verify(newController, Mockito.never()).close();
+ }
+
+ @Test
+ public void testControllerPublishedAfterDropIsClosedInsteadOfCached() {
+ CatalogAccessController defaultAccessController =
Mockito.mock(CatalogAccessController.class);
+ CatalogAccessController orphanController =
Mockito.mock(CatalogAccessController.class);
+ AccessControllerFactory factory =
Mockito.mock(AccessControllerFactory.class);
+ AccessControllerManager manager =
createAccessControllerManager(defaultAccessController);
+ ConcurrentHashMap<String, AccessControllerFactory> factories =
+ Deencapsulation.getField(manager,
"accessControllerFactoriesCache");
+ factories.put("test-controller", factory);
+
Mockito.when(factory.createAccessController(ImmutableMap.of())).thenReturn(orphanController);
+ ExternalCatalog droppedCatalog = mockCatalog("dropped", 20L);
+
+ CatalogMgr catalogMgr = Mockito.mock(CatalogMgr.class);
+ try (MockedStatic<Env> mockedEnv = Mockito.mockStatic(Env.class)) {
+ Env env = Mockito.mock(Env.class);
+ mockedEnv.when(Env::getCurrentEnv).thenReturn(env);
+ Mockito.when(env.getCatalogMgr()).thenReturn(catalogMgr);
+ Mockito.when(catalogMgr.getCatalog("dropped")).thenReturn(null);
+
+ manager.createAccessController(droppedCatalog, "test-controller",
ImmutableMap.of(), false);
+ }
+
+ Mockito.verify(orphanController).close();
+ Assert.assertFalse(manager.checkIfAccessControllerExist("dropped"));
+ }
+
+ @Test
+ public void testRemovingFallbackAliasDoesNotCloseSharedDefaultController()
{
+ CatalogAccessController defaultAccessController =
Mockito.mock(CatalogAccessController.class);
+ AccessControllerManager manager =
createAccessControllerManager(defaultAccessController);
+ ExternalCatalog catalog = mockCatalog("fallback", 30L);
+
+ CatalogMgr catalogMgr = Mockito.mock(CatalogMgr.class);
+ try (MockedStatic<Env> mockedEnv = Mockito.mockStatic(Env.class)) {
+ Env env = Mockito.mock(Env.class);
+ mockedEnv.when(Env::getCurrentEnv).thenReturn(env);
+ Mockito.when(env.getCatalogMgr()).thenReturn(catalogMgr);
+
Mockito.when(catalogMgr.getCatalog("fallback")).thenReturn(catalog);
+
+ Assert.assertSame(defaultAccessController,
manager.getAccessControllerOrDefault("fallback"));
+ manager.removeAccessController("fallback", catalog.getId());
+ }
+
+ Mockito.verify(defaultAccessController, Mockito.never()).close();
+ }
+
+ private ExternalCatalog mockCatalog(String name, long id) {
+ ExternalCatalog catalog = Mockito.mock(ExternalCatalog.class);
+ Mockito.when(catalog.getName()).thenReturn(name);
+ Mockito.when(catalog.getId()).thenReturn(id);
+ return catalog;
+ }
+
+ private void withCurrentCatalog(ExternalCatalog catalog, Runnable action) {
+ CatalogMgr catalogMgr = Mockito.mock(CatalogMgr.class);
+ try (MockedStatic<Env> mockedEnv = Mockito.mockStatic(Env.class)) {
+ Env env = Mockito.mock(Env.class);
+ mockedEnv.when(Env::getCurrentEnv).thenReturn(env);
+ Mockito.when(env.getCatalogMgr()).thenReturn(catalogMgr);
+
Mockito.when(catalogMgr.getCatalog(catalog.getName())).thenReturn(catalog);
+ action.run();
+ }
+ }
+
private AccessControllerManager
createAccessControllerManager(CatalogAccessController defaultAccessController) {
AccessControllerManager accessControllerManager = new
AccessControllerManager(new Auth());
Deencapsulation.setField(accessControllerManager,
"defaultAccessController", defaultAccessController);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]