This is an automated email from the ASF dual-hosted git repository.

mikexue pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/eventmesh.git


The following commit(s) were added to refs/heads/master by this push:
     new fcedbbc1c [ISSUE #4341] WebHookConfig failed to load after being 
inserted (#4344)
fcedbbc1c is described below

commit fcedbbc1caa01b5f441c24c61ddcce3d1b29d924
Author: Pil0tXia <[email protected]>
AuthorDate: Fri Aug 25 09:39:42 2023 +0800

    [ISSUE #4341] WebHookConfig failed to load after being inserted (#4344)
    
    * chore: add final to cacheWebHookConfig
    
    * fix: write cache only when MODIFY to avoid write empty config when CREATE
    
    * chore: correct log grammar mistake
    
    * feat: add try-catch and comments
    
    * fix: Wait for the notification of file write completion before 
initializing the cache
    
    * fix: use a shared latch
    
    * doc: add comment for cacheInit to explain CountDownLatch
    
    * feat: add object lock to ensure the atomicity of countDown in case of 
concurrency
    
    * feat: use synchronized instead of CountDownLatch
    
    * fix: expand synchronize zone to avoid file deleted before cacheInit
    
    * fix checkstyle
---
 .../webhook/admin/FileWebHookConfigOperation.java  | 46 ++++++++++++----------
 .../webhook/api/common/SharedLatchHolder.java      | 25 ++++++++++++
 .../receive/storage/WebhookFileListener.java       | 46 ++++++++++++++--------
 3 files changed, 80 insertions(+), 37 deletions(-)

diff --git 
a/eventmesh-webhook/eventmesh-webhook-admin/src/main/java/org/apache/eventmesh/webhook/admin/FileWebHookConfigOperation.java
 
b/eventmesh-webhook/eventmesh-webhook-admin/src/main/java/org/apache/eventmesh/webhook/admin/FileWebHookConfigOperation.java
index fd8535a53..db222905c 100644
--- 
a/eventmesh-webhook/eventmesh-webhook-admin/src/main/java/org/apache/eventmesh/webhook/admin/FileWebHookConfigOperation.java
+++ 
b/eventmesh-webhook/eventmesh-webhook-admin/src/main/java/org/apache/eventmesh/webhook/admin/FileWebHookConfigOperation.java
@@ -21,6 +21,7 @@ import org.apache.eventmesh.common.utils.JsonUtils;
 import org.apache.eventmesh.webhook.api.WebHookConfig;
 import org.apache.eventmesh.webhook.api.WebHookConfigOperation;
 import org.apache.eventmesh.webhook.api.WebHookOperationConstant;
+import org.apache.eventmesh.webhook.api.common.SharedLatchHolder;
 import org.apache.eventmesh.webhook.api.utils.ClassUtils;
 
 import java.io.BufferedReader;
@@ -88,7 +89,7 @@ public class FileWebHookConfigOperation implements 
WebHookConfigOperation {
         final File webhookConfigFile = getWebhookConfigFile(webHookConfig);
         if (!webhookConfigFile.exists()) {
             if (log.isErrorEnabled()) {
-                log.error("webhookConfig {} is not existed", 
webHookConfig.getCallbackPath());
+                log.error("webhookConfig {} does not exist", 
webHookConfig.getCallbackPath());
             }
             return 0;
         }
@@ -97,14 +98,16 @@ public class FileWebHookConfigOperation implements 
WebHookConfigOperation {
 
     @Override
     public Integer deleteWebHookConfig(final WebHookConfig webHookConfig) {
-        final File webhookConfigFile = getWebhookConfigFile(webHookConfig);
-        if (!webhookConfigFile.exists()) {
-            if (log.isErrorEnabled()) {
-                log.error("webhookConfig {} is not existed", 
webHookConfig.getCallbackPath());
+        synchronized (SharedLatchHolder.lock) {
+            final File webhookConfigFile = getWebhookConfigFile(webHookConfig);
+            if (!webhookConfigFile.exists()) {
+                if (log.isErrorEnabled()) {
+                    log.error("webhookConfig {} does not exist", 
webHookConfig.getCallbackPath());
+                }
+                return 0;
             }
-            return 0;
+            return webhookConfigFile.delete() ? 1 : 0;
         }
-        return webhookConfigFile.delete() ? 1 : 0;
     }
 
     /**
@@ -115,7 +118,7 @@ public class FileWebHookConfigOperation implements 
WebHookConfigOperation {
         final File webhookConfigFile = getWebhookConfigFile(webHookConfig);
         if (!webhookConfigFile.exists()) {
             if (log.isErrorEnabled()) {
-                log.error("webhookConfig {} is not existed", 
webHookConfig.getCallbackPath());
+                log.error("webhookConfig {} does not exist", 
webHookConfig.getCallbackPath());
             }
             return null;
         }
@@ -131,7 +134,7 @@ public class FileWebHookConfigOperation implements 
WebHookConfigOperation {
         final File manuDir = new File(manuDirPath);
         if (!manuDir.exists()) {
             if (log.isWarnEnabled()) {
-                log.warn("webhookConfig dir {} is not existed", manuDirPath);
+                log.warn("webhookConfig dir {} does not exist", manuDirPath);
             }
             return new ArrayList<>();
         }
@@ -164,7 +167,7 @@ public class FileWebHookConfigOperation implements 
WebHookConfigOperation {
             }
         } catch (IOException e) {
             if (log.isErrorEnabled()) {
-                log.error("get webhook from file {} error", 
webhookConfigFile.getPath(), e);
+                log.error("get webHookConfig from file {} error", 
webhookConfigFile.getPath(), e);
             }
             return null;
         }
@@ -173,18 +176,21 @@ public class FileWebHookConfigOperation implements 
WebHookConfigOperation {
     }
 
     public static boolean writeToFile(final File webhookConfigFile, final 
WebHookConfig webHookConfig) {
-        try (FileOutputStream fos = new FileOutputStream(webhookConfigFile);
-            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(fos, 
StandardCharsets.UTF_8))) {
-            // lock this file, and will auto release after fos close
-            fos.getChannel().lock();
-            
bw.write(Objects.requireNonNull(JsonUtils.toJSONString(webHookConfig)));
-        } catch (IOException e) {
-            if (log.isErrorEnabled()) {
-                log.error("write webhookConfig {} to file error", 
webHookConfig.getCallbackPath());
+        // Wait for the previous cacheInit to complete in case of concurrency
+        synchronized (SharedLatchHolder.lock) {
+            try (FileOutputStream fos = new 
FileOutputStream(webhookConfigFile);
+                BufferedWriter bw = new BufferedWriter(new 
OutputStreamWriter(fos, StandardCharsets.UTF_8))) {
+                // Lock this file to prevent concurrent modification and it 
will be automatically unlocked when fos closes
+                fos.getChannel().lock();
+                
bw.write(Objects.requireNonNull(JsonUtils.toJSONString(webHookConfig)));
+            } catch (IOException e) {
+                if (log.isErrorEnabled()) {
+                    log.error("write webhookConfig {} to file error", 
webHookConfig.getCallbackPath());
+                }
+                return false;
             }
-            return false;
+            return true;
         }
-        return true;
     }
 
     private String getWebhookConfigManuDir(final WebHookConfig webHookConfig) {
diff --git 
a/eventmesh-webhook/eventmesh-webhook-api/src/main/java/org/apache/eventmesh/webhook/api/common/SharedLatchHolder.java
 
b/eventmesh-webhook/eventmesh-webhook-api/src/main/java/org/apache/eventmesh/webhook/api/common/SharedLatchHolder.java
new file mode 100644
index 000000000..c59ff54b3
--- /dev/null
+++ 
b/eventmesh-webhook/eventmesh-webhook-api/src/main/java/org/apache/eventmesh/webhook/api/common/SharedLatchHolder.java
@@ -0,0 +1,25 @@
+/*
+ * 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.eventmesh.webhook.api.common;
+
+public class SharedLatchHolder {
+
+    // secure the execution sequence of writeToFile and cacheInit
+    public static final Object lock = new Object();
+
+}
diff --git 
a/eventmesh-webhook/eventmesh-webhook-receive/src/main/java/org/apache/eventmesh/webhook/receive/storage/WebhookFileListener.java
 
b/eventmesh-webhook/eventmesh-webhook-receive/src/main/java/org/apache/eventmesh/webhook/receive/storage/WebhookFileListener.java
index 5461cdcbb..8d42c9f19 100644
--- 
a/eventmesh-webhook/eventmesh-webhook-receive/src/main/java/org/apache/eventmesh/webhook/receive/storage/WebhookFileListener.java
+++ 
b/eventmesh-webhook/eventmesh-webhook-receive/src/main/java/org/apache/eventmesh/webhook/receive/storage/WebhookFileListener.java
@@ -24,6 +24,7 @@ import static 
java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
 import org.apache.eventmesh.common.utils.JsonUtils;
 import org.apache.eventmesh.webhook.api.WebHookConfig;
 import org.apache.eventmesh.webhook.api.WebHookOperationConstant;
+import org.apache.eventmesh.webhook.api.common.SharedLatchHolder;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -51,7 +52,7 @@ public class WebhookFileListener {
     private final transient Set<String> pathSet = new LinkedHashSet<>(); // 
monitored subdirectory
     private final transient Map<WatchKey, String> watchKeyPathMap = new 
ConcurrentHashMap<>(); // WatchKey's path
     private transient String filePath;
-    private transient Map<String, WebHookConfig> cacheWebHookConfig;
+    private final transient Map<String, WebHookConfig> cacheWebHookConfig;
 
     public WebhookFileListener(final String filePath, final Map<String, 
WebHookConfig> cacheWebHookConfig) {
         this.filePath = WebHookOperationConstant.getFilePath(filePath);
@@ -90,20 +91,21 @@ public class WebhookFileListener {
     }
 
     /**
-     * Read the file and cache it in map
+     * Read the file and cache it in local map for manufacturers webhook 
payload delivery
+     * <p>
+     * A shared lock is used to ensure that this method should be invoked 
after the {@code webhookConfigFile} is written completely
+     * by {@code 
org.apache.eventmesh.webhook.admin.FileWebHookConfigOperation#writeToFile} when 
multiple modify events are triggered.
      *
      * @param webhookConfigFile webhookConfigFile
      */
     private void cacheInit(final File webhookConfigFile) {
         final StringBuilder fileContent = new StringBuilder();
-        try (BufferedReader br = 
Files.newBufferedReader(Paths.get(webhookConfigFile.getAbsolutePath()),
-            StandardCharsets.UTF_8)) {
+        try (BufferedReader br = 
Files.newBufferedReader(Paths.get(webhookConfigFile.getAbsolutePath()), 
StandardCharsets.UTF_8)) {
             while (br.ready()) {
                 fileContent.append(br.readLine());
             }
-
         } catch (IOException e) {
-            log.error("cacheInit failed", e);
+            log.error("cacheInit buffer read failed", e);
         }
         final WebHookConfig webHookConfig = 
JsonUtils.parseObject(fileContent.toString(), WebHookConfig.class);
         cacheWebHookConfig.put(webhookConfigFile.getName(), webHookConfig);
@@ -143,36 +145,46 @@ public class WebhookFileListener {
                 WatchKey key = null;
                 try {
                     assert service != null;
+                    // The code will block here until a file system event 
occurs
                     key = service.take();
                 } catch (InterruptedException e) {
                     log.error("Interrupted", e);
                 }
 
                 assert key != null;
+                // A newly created config file will be captured for two 
events, ENTRY_CREATE and ENTRY_MODIFY
                 for (final WatchEvent<?> event : key.pollEvents()) {
                     final String flashPath = watchKeyPathMap.get(key);
                     // manufacturer change
-                    final String path = 
flashPath.concat("/").concat(event.context().toString());
+                    final String path = 
flashPath.concat(WebHookOperationConstant.FILE_SEPARATOR).concat(event.context().toString());
                     final File file = new File(path);
-                    if (ENTRY_CREATE == event.kind() || ENTRY_MODIFY == 
event.kind()) {
-                        if (file.isFile()) {
-                            cacheInit(file);
-                        } else {
+                    // Wait for file write/delete completion before 
initializing the cache
+                    synchronized (SharedLatchHolder.lock) {
+                        if (file.isDirectory() && (ENTRY_CREATE == 
event.kind() || ENTRY_MODIFY == event.kind())) {
+                            // If it is a folder, re-register the listener
                             try {
                                 key = Paths.get(path).register(service, 
ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
                                 watchKeyPathMap.put(key, path);
                             } catch (IOException e) {
                                 log.error("registerWatchKey failed", e);
                             }
-                        }
-                    } else if (ENTRY_DELETE == event.kind()) {
-                        if (file.isDirectory()) {
-                            watchKeyPathMap.remove(key);
-                        } else {
-                            deleteConfig(file);
+                        } else if (file.isFile() && ENTRY_MODIFY == 
event.kind()) {
+                            // If it is a file, cache it only when it is 
modified to wait for complete file writes
+                            try {
+                                cacheInit(file);
+                            } catch (Exception e) {
+                                log.error("cacheInit failed", e);
+                            }
+                        } else if (ENTRY_DELETE == event.kind()) {
+                            if (file.isDirectory()) {
+                                watchKeyPathMap.remove(key);
+                            } else {
+                                deleteConfig(file);
+                            }
                         }
                     }
                 }
+                // Reset the WatchKey to receive subsequent file system events
                 if (!key.reset()) {
                     break;
                 }


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

Reply via email to