7164570: (fs) WatchService queues CREATE event but not DELETE event for very short lived files [sol11]
authoralanb
Tue, 01 May 2012 11:17:20 +0100
changeset 12556 2e06461600d9
parent 12555 44798b7530d1
child 12557 30cadbf1ecbd
7164570: (fs) WatchService queues CREATE event but not DELETE event for very short lived files [sol11] Reviewed-by: chegar
jdk/src/solaris/classes/sun/nio/fs/SolarisWatchService.java
jdk/test/java/nio/file/WatchService/MayFlies.java
--- a/jdk/src/solaris/classes/sun/nio/fs/SolarisWatchService.java	Fri Apr 27 04:25:50 2012 -0700
+++ b/jdk/src/solaris/classes/sun/nio/fs/SolarisWatchService.java	Tue May 01 11:17:20 2012 +0100
@@ -314,7 +314,7 @@
             fileKey2WatchKey.put(fileKey, watchKey);
 
             // register all entries in directory
-            registerChildren(dir, watchKey, false);
+            registerChildren(dir, watchKey, false, false);
 
             return watchKey;
         }
@@ -486,7 +486,8 @@
         void processDirectoryEvents(SolarisWatchKey key, int mask) {
             if ((mask & (FILE_MODIFIED | FILE_ATTRIB)) != 0) {
                 registerChildren(key.getDirectory(), key,
-                    key.events().contains(StandardWatchEventKinds.ENTRY_CREATE));
+                    key.events().contains(StandardWatchEventKinds.ENTRY_CREATE),
+                    key.events().contains(StandardWatchEventKinds.ENTRY_DELETE));
             }
         }
 
@@ -535,14 +536,16 @@
         /**
          * Registers all entries in the given directory
          *
-         * The {@code sendEvents} parameter indicates if ENTRY_CREATE events
-         * should be queued when new entries are found. When initially
-         * registering a directory then will always be false. When re-scanning
-         * a directory then it depends on if the event is enabled or not.
+         * The {@code sendCreateEvents} and {@code sendDeleteEvents} parameters
+         * indicates if ENTRY_CREATE and ENTRY_DELETE events should be queued
+         * when new entries are found. When initially registering a directory
+         * they will always be false. When re-scanning a directory then it
+         * depends on if the events are enabled or not.
          */
         void registerChildren(UnixPath dir,
                               SolarisWatchKey parent,
-                              boolean sendEvents)
+                              boolean sendCreateEvents,
+                              boolean sendDeleteEvents)
         {
             // if the ENTRY_MODIFY event is not enabled then we don't need
             // modification events for entries in the directory
@@ -550,14 +553,7 @@
             if (parent.events().contains(StandardWatchEventKinds.ENTRY_MODIFY))
                 events |= (FILE_MODIFIED | FILE_ATTRIB);
 
-            DirectoryStream<Path> stream = null;
-            try {
-                stream = Files.newDirectoryStream(dir);
-            } catch (IOException x) {
-                // nothing we can do
-                return;
-            }
-            try {
+            try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
                 for (Path entry: stream) {
                     Path name = entry.getFileName();
 
@@ -565,32 +561,34 @@
                     if (parent.getChild(name) != null)
                         continue;
 
-                    // send ENTRY_CREATE if enabled
-                    if (sendEvents) {
-                        parent.signalEvent(StandardWatchEventKinds.ENTRY_CREATE, name);
-                    }
-
-                    // register it
+                    // attempt to register entry
                     long object = 0L;
+                    int errno = 0;
                     try {
                         object = registerImpl((UnixPath)entry, events);
                     } catch (UnixException x) {
-                        // can't register so ignore for now.
-                        continue;
+                        errno = x.errno();
                     }
 
-                    // create node
-                    EntryNode node = new EntryNode(object, entry.getFileName(), parent);
-                    // tell the parent about it
-                    parent.addChild(entry.getFileName(), node);
-                    object2Node.put(object, node);
+                    boolean registered = (object != 0L);
+                    boolean deleted = (errno == ENOENT);
+
+                    if (registered) {
+                        // create node
+                        EntryNode node = new EntryNode(object, entry.getFileName(), parent);
+                        // tell the parent about it
+                        parent.addChild(entry.getFileName(), node);
+                        object2Node.put(object, node);
+                    }
+
+                    if (sendCreateEvents && (registered || deleted))
+                        parent.signalEvent(StandardWatchEventKinds.ENTRY_CREATE, name);
+                    if (sendDeleteEvents && deleted)
+                        parent.signalEvent(StandardWatchEventKinds.ENTRY_DELETE, name);
+
                 }
-            } catch (ConcurrentModificationException x) {
-                // error during iteration which we ignore for now
-            } finally {
-                try {
-                    stream.close();
-                } catch (IOException x) { }
+            } catch (DirectoryIteratorException | IOException x) {
+                // nothing we can do
             }
         }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/java/nio/file/WatchService/MayFlies.java	Tue May 01 11:17:20 2012 +0100
@@ -0,0 +1,181 @@
+/*
+ * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/* @test
+ * @bug 7164570
+ * @summary Test that CREATE and DELETE events are paired for very
+ *     short lived files
+ * @library ..
+ * @run main MayFlies
+ */
+
+import java.nio.file.*;
+import static java.nio.file.StandardWatchEventKinds.*;
+import java.util.*;
+import java.util.concurrent.*;
+
+public class MayFlies {
+
+    static volatile boolean stopped;
+
+    static volatile boolean failure;
+
+    /**
+     * Continuously creates short-lived files in a directory until {@code
+     * stopped} is set to {@code true}.
+     */
+    static class MayFlyHatcher implements Runnable {
+        static final Random rand = new Random();
+
+        private final Path dir;
+        private final String prefix;
+
+        private MayFlyHatcher(Path dir, String prefix) {
+            this.dir = dir;
+            this.prefix = prefix;
+        }
+
+        static void start(Path dir, String prefix) {
+            MayFlyHatcher hatcher = new MayFlyHatcher(dir, prefix);
+            new Thread(hatcher).start();
+        }
+
+        public void run() {
+            try {
+                int n = 0;
+                while (!stopped) {
+                    Path name = dir.resolve(prefix + (++n));
+                    Files.createFile(name);
+                    if (rand.nextBoolean())
+                        Thread.sleep(rand.nextInt(500));
+                    Files.delete(name);
+                    Thread.sleep(rand.nextInt(100));
+                }
+                System.out.format("%d %ss hatched%n", n, prefix);
+            } catch (Exception x) {
+                failure = true;
+                x.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * Test phases.
+     */
+    static enum Phase {
+        /**
+         * Short-lived files are being created
+         */
+        RUNNING,
+        /**
+         * Draining the final events
+         */
+        FINISHING,
+        /**
+         * No more events or overflow detected
+         */
+        FINISHED
+    };
+
+
+    public static void main(String[] args) throws Exception {
+
+        // schedules file creation to stop after 10 seconds
+        ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();
+        pool.schedule(
+            new Runnable() { public void run() { stopped = true; }},
+            10, TimeUnit.SECONDS);
+
+        Path dir = TestUtil.createTemporaryDirectory();
+
+        Set<Path> entries = new HashSet<>();
+        int nCreateEvents = 0;
+        boolean overflow = false;
+
+        try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
+            WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE);
+
+            // start hatching Mayflies
+            MayFlyHatcher.start(dir, "clinger");
+            MayFlyHatcher.start(dir, "crawler");
+            MayFlyHatcher.start(dir, "burrower");
+            MayFlyHatcher.start(dir, "swimmer");
+
+            Phase phase = Phase.RUNNING;
+            while (phase != Phase.FINISHED) {
+                // during the running phase then poll for 1 second.
+                // once the file creation has stopped then move to the finishing
+                // phase where we do a long poll to ensure that all events have
+                // been read.
+                int time = (phase == Phase.RUNNING) ? 1 : 15;
+                key = watcher.poll(time, TimeUnit.SECONDS);
+                if (key == null) {
+                    if (phase == Phase.RUNNING && stopped)
+                        phase = Phase.FINISHING;
+                    else if (phase == Phase.FINISHING)
+                        phase = Phase.FINISHED;
+                } else {
+                    // process events
+                    for (WatchEvent<?> event: key.pollEvents()) {
+                        if (event.kind() == ENTRY_CREATE) {
+                            Path name = (Path)event.context();
+                            boolean added = entries.add(name);
+                            if (!added)
+                                throw new RuntimeException("Duplicate ENTRY_CREATE event");
+                            nCreateEvents++;
+                        } else if (event.kind() == ENTRY_DELETE) {
+                            Path name = (Path)event.context();
+                            boolean removed = entries.remove(name);
+                            if (!removed)
+                                throw new RuntimeException("ENTRY_DELETE event without ENTRY_CREATE event");
+                        } else if (event.kind() == OVERFLOW) {
+                            overflow = true;
+                            phase = Phase.FINISHED;
+                        } else {
+                            throw new RuntimeException("Unexpected event: " + event.kind());
+                        }
+                    }
+                    key.reset();
+                }
+            }
+
+            System.out.format("%d ENTRY_CREATE events read%n", nCreateEvents);
+
+            // there should be a DELETE event for each CREATE event and so the
+            // entries set should be empty.
+            if (!overflow && !entries.isEmpty())
+                throw new RuntimeException("Missed " + entries.size() + " DELETE event(s)");
+
+
+        } finally {
+            try {
+                TestUtil.removeAll(dir);
+            } finally {
+                pool.shutdown();
+            }
+        }
+
+        if (failure)
+            throw new RuntimeException("Test failed - see log file for details");
+    }
+}