github-actions[bot] commented on code in PR #65670:
URL: https://github.com/apache/doris/pull/65670#discussion_r3592854751


##########
fe/fe-core/src/main/java/org/apache/doris/catalog/Resource.java:
##########
@@ -350,12 +357,46 @@ public Resource clone() {
         return copied;
     }
 
-    private void notifyUpdate(Map<String, String> properties) {
-        
references.entrySet().stream().collect(Collectors.groupingBy(Entry::getValue)).forEach((type,
 refs) -> {
-            if (type == ReferenceType.CATALOG) {
-                // No longer support resource in Catalog.
+    final Resource getCopiedResourceSnapshot() {
+        String json;
+        readLock();

Review Comment:
   [P1] Pair this snapshot lock with every mutable resource writer
   
   This read lock only excludes mutations that take `Resource.writeLock()`. 
`HMSResource.modifyProperties()`, `OdbcCatalogResource.modifyProperties()`, and 
`JdbcResource.modifyProperties()` all write their serialized `HashMap`s without 
that lock, and all three types are reachable through `ALTER RESOURCE`. A live 
`ResourceMgr.write()` can therefore hold this read lock while one of those maps 
changes, so Gson can emit a mixed image or fail with 
`ConcurrentModificationException`. HDFS is paired now, but the new generic 
snapshot guarantee is still false for these parallel subtype paths. Please make 
those writers/readers use the same lock protocol (or provide subtype snapshot 
hooks) and add an image-versus-ALTER test for at least one currently unlocked 
type.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/ResourceMgr.java:
##########
@@ -145,20 +181,40 @@ public void replayDropResource(DropResourceOperationLog 
operationLog) {
     }
 
     public void alterResource(String resourceName, Map<String, String> 
properties) throws DdlException {
-        if (!nameToResource.containsKey(resourceName)) {
+        Resource resource = nameToResource.get(resourceName);
+        if (resource == null) {
             throw new DdlException("Resource(" + resourceName + ") dose not 
exist.");
         }
 
-        Resource resource = nameToResource.get(resourceName);
-        resource.modifyProperties(properties);
+        Resource logResource;
+        EditLog.EditLogItem logItem;
+        synchronized (resource.getAlterLock()) {
+            if (nameToResource.get(resourceName) != resource) {
+                throw new DdlException("Resource(" + resourceName + ") changed 
concurrently");
+            }
+            resource.modifyProperties(properties);

Review Comment:
   [P1] Alter the same Resource instance that was validated
   
   `AlterResourceCommand.validate()` fetches one Resource and runs that 
subtype's `checkProperties()`, but then calls this manager by name and this 
method performs a fresh lookup. A DROP/recreate between those calls means the 
new identity check only protects the replacement after the second lookup; it 
does not prove this is the object that was validated. For example, an ODBC 
`host` change can validate against ODBC A, then be applied and journaled on a 
same-name HDFS B, whose `modifyProperties()` writes the foreign nonempty key 
without revalidating it. Pass the validated identity through and recheck it 
under `alterLock`, or re-run validation on the locked current object, with a 
cross-type replacement test.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/ResourceMgr.java:
##########
@@ -145,20 +181,40 @@ public void replayDropResource(DropResourceOperationLog 
operationLog) {
     }
 
     public void alterResource(String resourceName, Map<String, String> 
properties) throws DdlException {
-        if (!nameToResource.containsKey(resourceName)) {
+        Resource resource = nameToResource.get(resourceName);
+        if (resource == null) {
             throw new DdlException("Resource(" + resourceName + ") dose not 
exist.");
         }
 
-        Resource resource = nameToResource.get(resourceName);
-        resource.modifyProperties(properties);
+        Resource logResource;
+        EditLog.EditLogItem logItem;
+        synchronized (resource.getAlterLock()) {
+            if (nameToResource.get(resourceName) != resource) {
+                throw new DdlException("Resource(" + resourceName + ") changed 
concurrently");
+            }
+            resource.modifyProperties(properties);
+            // Keep mutation, snapshot, and submission ordered without holding 
the resource state lock.
+            logResource = getAlterLogResource(resource);
+            logItem = 
Env.getCurrentEnv().getEditLog().submitEdit(OperationType.OP_ALTER_RESOURCE, 
logResource);
+        }
 
-        // log alter
-        Env.getCurrentEnv().getEditLog().logAlterResource(resource);
-        LOG.info("Alter resource success. Resource: {}", resource);
+        logItem.await();
+        LOG.info("Alter resource success. Resource: {}", logResource);
     }
 
     public void replayAlterResource(Resource resource) {
-        nameToResource.put(resource.getName(), resource);
+        Resource replayed = 
nameToResource.computeIfPresent(resource.getName(), (name, currentResource) -> {

Review Comment:
   [P1] Preserve ALTERs for deferred restore-created resources
   
   Absence during replay does not always mean the ALTER is stale. The earlier 
PENDING `RestoreJob` record is durable, but ordinary replay only re-registers 
that deferred job; it does not run the resource creation. Live PENDING 
execution then publishes an ODBC resource through `createResource(Resource, 
false)` without a new creation-bearing state log. That visible resource can be 
altered and the ALTER acknowledged; if the FE crashes before the next 
restore-state log, replay reaches this method with the resource absent and 
discards the ALTER, then the resumed job recreates the original backup 
resource. Please journal/register restore-created resources before exposing 
them to ALTER, or defer and later apply the matching ALTER; add a recovery test 
for this PENDING window.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/ResourceMgr.java:
##########
@@ -108,35 +125,54 @@ public void replayCreateResource(Resource resource) {
 
     public void dropResource(DropResourceCommand dropResourceCommand) throws 
DdlException {
         String resourceName = dropResourceCommand.getResourceName();
-        if (!nameToResource.containsKey(resourceName)) {
+        Resource resource = nameToResource.get(resourceName);
+        if (resource == null) {
             if (dropResourceCommand.isIfExists()) {
                 return;
             }
             throw new DdlException("Resource(" + resourceName + ") does not 
exist");
         }
 
-        Resource resource = nameToResource.get(resourceName);
-        resource.dropResource();
-
-        // Check whether the resource is in use before deleting it, except 
spark resource
-        StoragePolicy checkedStoragePolicy = StoragePolicy.ofCheck(null);
-        checkedStoragePolicy.setStorageResource(resourceName);
-        if 
(Env.getCurrentEnv().getPolicyMgr().existPolicy(checkedStoragePolicy)) {
-            Policy policy = 
Env.getCurrentEnv().getPolicyMgr().getPolicy(checkedStoragePolicy);
-            LOG.warn("Can not drop resource, since it's used in policy {}", 
policy.getPolicyName());
-            throw new DdlException("Can not drop resource, since it's used in 
policy " + policy.getPolicyName());
+        EditLog.EditLogItem logItem;
+        synchronized (resource.getAlterLock()) {
+            if (nameToResource.get(resourceName) != resource) {
+                if (dropResourceCommand.isIfExists() && 
!nameToResource.containsKey(resourceName)) {
+                    return;
+                }
+                throw new DdlException("Resource(" + resourceName + ") changed 
concurrently");
+            }
+
+            resource.dropResource();

Review Comment:
   [P1] Serialize policy reference acquisition with resource DROP
   
   The reference check here is synchronized only for the duration of 
`Resource.dropResource()`, while `StoragePolicy.init()` adds its reference 
before the later `PolicyMgr.createPolicy()` publish/journal. A concrete race 
is: DROP observes no references; CREATE POLICY adds `p@POLICY` and pauses 
before PolicyMgr insertion; DROP's policy lookup still sees nothing, submits 
the DROP, and removes the resource; CREATE POLICY then journals successfully. 
Replay leaves a durable policy whose resource is absent. This is distinct from 
ALTER payload reference ownership. Please make policy reference 
acquisition/policy journaling and resource DROP share one lifecycle ordering 
protocol with an identity recheck, and cover this race with latches.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/ResourceMgr.java:
##########
@@ -82,23 +84,38 @@ public void createResource(CreateResourceCommand command) 
throws DdlException {
                     "Only support SPARK, ODBC_CATALOG, JDBC, S3_COOLDOWN, S3, 
HDFS(JFS/JUICEFS), and HMS resource.");
         }
         Resource resource = Resource.fromCommand(command);
-        if (createResource(resource, info.isIfNotExists())) {
-            Env.getCurrentEnv().getEditLog().logCreateResource(resource);
-            LOG.info("Create resource success. Resource: {}", 
resource.getName());
+        Resource logResource;
+        EditLog.EditLogItem logItem;
+        synchronized (nameToResource) {
+            if (nameToResource.containsKey(resource.getName())) {
+                if (info.isIfNotExists()) {
+                    return;
+                }
+                throw new DdlException("Resource(" + resource.getName() + ") 
already exist");
+            }
+            logResource = getCreateLogResource(resource);
+            // Queue CREATE before publishing the live object so dependent 
operations cannot journal first.
+            logItem = Env.getCurrentEnv().getEditLog()

Review Comment:
   [P1] Keep live image membership aligned with submitted journal IDs
   
   CREATE now submits `OP_CREATE_RESOURCE` before `nameToResource.put`, and 
DROP submits before `remove`. In direct mode that record is already durable 
when `submitEdit()` returns; in batch mode the flusher can also persist it 
immediately. If `Env.dumpImage()` selects that max journal ID in the 
intervening window, the body omits the CREATE (or still contains the DROP). 
Recovery trusts the `image.<max-id>` filename sequence in `loadImage()`—the 
older embedded header value is only checksummed—so it starts after that record 
and permanently loses or resurrects the resource. Please coordinate the 
live-image boundary with these lifecycle map transitions and add direct/batch 
recovery tests paused after persistence but before put/remove.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/ResourceMgr.java:
##########
@@ -108,35 +125,54 @@ public void replayCreateResource(Resource resource) {
 
     public void dropResource(DropResourceCommand dropResourceCommand) throws 
DdlException {
         String resourceName = dropResourceCommand.getResourceName();
-        if (!nameToResource.containsKey(resourceName)) {
+        Resource resource = nameToResource.get(resourceName);
+        if (resource == null) {
             if (dropResourceCommand.isIfExists()) {
                 return;
             }
             throw new DdlException("Resource(" + resourceName + ") does not 
exist");
         }
 
-        Resource resource = nameToResource.get(resourceName);
-        resource.dropResource();
-
-        // Check whether the resource is in use before deleting it, except 
spark resource
-        StoragePolicy checkedStoragePolicy = StoragePolicy.ofCheck(null);
-        checkedStoragePolicy.setStorageResource(resourceName);
-        if 
(Env.getCurrentEnv().getPolicyMgr().existPolicy(checkedStoragePolicy)) {
-            Policy policy = 
Env.getCurrentEnv().getPolicyMgr().getPolicy(checkedStoragePolicy);
-            LOG.warn("Can not drop resource, since it's used in policy {}", 
policy.getPolicyName());
-            throw new DdlException("Can not drop resource, since it's used in 
policy " + policy.getPolicyName());
+        EditLog.EditLogItem logItem;
+        synchronized (resource.getAlterLock()) {
+            if (nameToResource.get(resourceName) != resource) {
+                if (dropResourceCommand.isIfExists() && 
!nameToResource.containsKey(resourceName)) {
+                    return;
+                }
+                throw new DdlException("Resource(" + resourceName + ") changed 
concurrently");
+            }
+
+            resource.dropResource();
+
+            // Check whether the resource is in use before deleting it, except 
spark resource
+            StoragePolicy checkedStoragePolicy = StoragePolicy.ofCheck(null);
+            checkedStoragePolicy.setStorageResource(resourceName);
+            if 
(Env.getCurrentEnv().getPolicyMgr().existPolicy(checkedStoragePolicy)) {
+                Policy policy = 
Env.getCurrentEnv().getPolicyMgr().getPolicy(checkedStoragePolicy);
+                LOG.warn("Can not drop resource, since it's used in policy 
{}", policy.getPolicyName());
+                throw new DdlException("Can not drop resource, since it's used 
in policy " + policy.getPolicyName());
+            }
+
+            logItem = Env.getCurrentEnv().getEditLog().submitEdit(
+                    OperationType.OP_DROP_RESOURCE, new 
DropResourceOperationLog(resourceName));
+            if (!nameToResource.remove(resourceName, resource)) {
+                LOG.error("Fatal Error: failed to remove resource {} after 
submitting its drop log", resourceName);
+                System.exit(-1);
+                throw new IllegalStateException("failed to remove resource " + 
resourceName);
+            }
         }
-        nameToResource.remove(resourceName);
-        // log drop
-        Env.getCurrentEnv().getEditLog().logDropResource(new 
DropResourceOperationLog(resourceName));
+
+        logItem.await();
         LOG.info("Drop resource success. Resource resourceName: {}", 
resourceName);
     }
 
     // Drop resource whether successful or not
     public void dropResource(Resource resource) {
         String name = resource.getName();
-        if (nameToResource.remove(name) == null) {
-            LOG.info("resource " + name + " does not exists.");
+        synchronized (resource.getAlterLock()) {
+            if (!nameToResource.remove(name, resource)) {

Review Comment:
   [P1] Remove the logical restore-owned resource during replay
   
   This exact-object removal cannot reproduce restore cancellation across 
journal records. Replay of the DOWNLOAD `RestoreJob` inserts the Resource 
objects nested in that record, but a later COMMIT record is independently 
deserialized and becomes the current job without recreating those resources. 
When a subsequent CANCEL record replays, `replayCancel()` cleans up through the 
COMMIT job, so it passes a logically identical but different Resource instance 
here and `remove(name, resource)` silently fails. The CANCEL record has already 
cleared `restoredResources` and writes no separate resource DROP, leaving an 
orphan resource after recovery even though live cancellation removed it. Please 
match the current resource by stable restore ownership/ID under its lifecycle 
lock (while still protecting a true same-name replacement), and add a DOWNLOAD 
-> COMMIT -> CANCEL recovery test with a restored ODBC resource.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/ResourceMgr.java:
##########
@@ -145,20 +181,40 @@ public void replayDropResource(DropResourceOperationLog 
operationLog) {
     }
 
     public void alterResource(String resourceName, Map<String, String> 
properties) throws DdlException {
-        if (!nameToResource.containsKey(resourceName)) {
+        Resource resource = nameToResource.get(resourceName);
+        if (resource == null) {
             throw new DdlException("Resource(" + resourceName + ") dose not 
exist.");
         }
 
-        Resource resource = nameToResource.get(resourceName);
-        resource.modifyProperties(properties);
+        Resource logResource;
+        EditLog.EditLogItem logItem;
+        synchronized (resource.getAlterLock()) {

Review Comment:
   [P1] Recheck policy-sensitive fields after remote validation
   
   This `alterLock` is not taken by storage-policy reference acquisition. S3 
checks whether a policy reference exists before slow `pingS3()`, so ALTER can 
pass that guard and block in remote I/O; CREATE POLICY can then add and journal 
its reference; ALTER resumes and commits bucket/root/region/endpoint changes 
that S3 explicitly forbids once a policy uses the resource. Both journal orders 
replay to the same invalid combination. Please coordinate policy reference 
acquisition with the ALTER commit (or recheck atomically after validation) 
using a documented lock order that still keeps remote I/O outside the state 
lock, and add an S3 policy-versus-blocked-ping test.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to