diff -r 59d630fe8661 masterfs/apichanges.xml --- a/masterfs/apichanges.xml Mon Nov 19 12:07:47 2012 +0100 +++ b/masterfs/apichanges.xml Tue Nov 20 11:59:03 2012 +0100 @@ -49,6 +49,22 @@ MasterFileSystem API + + + A property to suspend native listeners + + + + + + A + way to temporarily suspend native listeners. + + + fileLocked method can throw IOException diff -r 59d630fe8661 masterfs/arch.xml --- a/masterfs/arch.xml Mon Nov 19 12:07:47 2012 +0100 +++ b/masterfs/arch.xml Tue Nov 20 11:59:03 2012 +0100 @@ -533,6 +533,36 @@ consumption, not for consumption by other parts of the system. The name and meaning of this property may change in any release. + + +

+ Native listeners check the org.netbeans.io.suspend property. + If it is set to integer greater than zero, they stop delivering file change events. + The list of modified directories is recorded (its size is made available + by setting its string value into org.netbeans.io.pending property), + but its processing is suspended. Events are delivered when + org.netbeans.io.suspend property changes its value to 0 + or becomes empty. +

+

+ I/O intensive operations in other NetBeans modules are advised + to honour the org.netbeans.io.suspend property as well and + suspend their I/O activities too. +

+

+ In order to properly communicate changes to the property between multiple + receivers and multiple controllers it is suggested to + only manipulate the value under synchronized("org.netbeans.io.suspend".intern()) + lock. Those changing the value are supposed to increment it by one when they + request the suspend and decrement it by one when they want to resume their + own suspend. +

+

+ Whenever a change to the state of the property is made, + controllers are supposed to + "org.netbeans.io.suspend".intern().notifyAll(). +

+
diff -r 59d630fe8661 masterfs/manifest.mf --- a/masterfs/manifest.mf Mon Nov 19 12:07:47 2012 +0100 +++ b/masterfs/manifest.mf Tue Nov 20 11:59:03 2012 +0100 @@ -1,7 +1,7 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.modules.masterfs/2 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/masterfs/resources/Bundle.properties -OpenIDE-Module-Specification-Version: 2.41 +OpenIDE-Module-Specification-Version: 2.42 OpenIDE-Module-Recommends: org.netbeans.modules.masterfs.providers.Notifier OpenIDE-Module-Provides: org.openide.filesystems.FileUtil.toFileObject AutoUpdate-Show-In-Client: false diff -r 59d630fe8661 masterfs/src/org/netbeans/modules/masterfs/watcher/Watcher.java --- a/masterfs/src/org/netbeans/modules/masterfs/watcher/Watcher.java Mon Nov 19 12:07:47 2012 +0100 +++ b/masterfs/src/org/netbeans/modules/masterfs/watcher/Watcher.java Tue Nov 20 11:59:03 2012 +0100 @@ -57,9 +57,11 @@ import org.netbeans.modules.masterfs.providers.ProvidedExtensions; import org.openide.filesystems.FileObject; import org.netbeans.modules.masterfs.providers.AnnotationProvider; +import org.openide.util.Exceptions; import org.openide.util.Lookup; import org.openide.util.Lookup.Item; import org.openide.util.RequestProcessor; +import org.openide.util.WeakSet; import org.openide.util.lookup.ServiceProvider; import org.openide.util.lookup.ServiceProviders; @@ -354,7 +356,7 @@ } } - private final Object lock = new Object(); + private final Object lock = "org.netbeans.io.suspend".intern(); private Set pending; // guarded by lock private static RequestProcessor RP = new RequestProcessor("Pending refresh", 1); @@ -364,10 +366,25 @@ Set toRefresh; synchronized(lock) { toRefresh = pending; - pending = null; if (toRefresh == null) { return; } + for (;;) { + int cnt = Integer.getInteger("org.netbeans.io.suspend", 0); // NOI18N + if (cnt <= 0) { + break; + } + final String pndngSize = String.valueOf(toRefresh.size()); + System.setProperty("org.netbeans.io.pending", pndngSize); // NOI18N + LOG.log(Level.FINE, "Suspend count {0} pending {1}", new Object[]{cnt, pndngSize}); + try { + lock.wait(1500); + } catch (InterruptedException ex) { + LOG.log(Level.FINE, null, ex); + } + } + System.getProperties().remove("org.netbeans.io.pending"); // NOI18N + pending = null; } LOG.log(Level.FINE, "Refreshing {0} directories", toRefresh.size()); @@ -391,7 +408,7 @@ synchronized(lock) { if (pending == null) { refreshTask.schedule(1500); - pending = new HashSet(); + pending = new WeakSet(); } pending.add(fo); } @@ -404,7 +421,7 @@ synchronized(lock) { if (pending == null) { refreshTask.schedule(1500); - pending = new HashSet(); + pending = new WeakSet(); } pending.addAll(fos); } diff -r 59d630fe8661 masterfs/test/unit/src/org/netbeans/modules/masterfs/watcher/WatcherSuspendTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/masterfs/test/unit/src/org/netbeans/modules/masterfs/watcher/WatcherSuspendTest.java Tue Nov 20 11:59:03 2012 +0100 @@ -0,0 +1,193 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2011 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2011 Sun Microsystems, Inc. + */ +package org.netbeans.modules.masterfs.watcher; + +import java.io.File; +import org.netbeans.modules.masterfs.providers.Notifier; +import java.io.IOException; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import org.netbeans.junit.MockServices; +import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.masterfs.filebasedfs.fileobjects.FileObjectFactory; +import org.netbeans.modules.masterfs.filebasedfs.naming.NamingFactory; +import org.openide.filesystems.FileChangeAdapter; +import org.openide.filesystems.FileEvent; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.Lookup; + +/** Test the behavior of Watcher. + * + * @author Jaroslav Tulach + */ +public class WatcherSuspendTest extends NbTestCase { + private TestNotifier notify; + private L listener; + private Watcher watcher; + + public WatcherSuspendTest(String s) { + super(s); + } + + @Override + protected void setUp() throws Exception { + clearWorkDir(); + MockServices.setServices(TestNotifier.class); + listener = new L(); + watcher = Lookup.getDefault().lookup(Watcher.class); + notify = Lookup.getDefault().lookup(TestNotifier.class); + notify.start(); + } + + @Override + protected void tearDown() throws Exception { + } + + public void testSuspendRequests() throws Exception { + FileObject root = FileUtil.toFileObject(getWorkDir()); + FileObject folder = root.createFolder("dir"); + File dir = FileUtil.toFile(folder); + + FileObject[] arr = folder.getChildren(); + folder.addFileChangeListener(listener); + assertEquals("Empty ", 0, arr.length); + + final String prop = "org.netbeans.io.suspend".intern(); + synchronized (prop) { + int prev = Integer.getInteger(prop, 0); + System.setProperty(prop, "" + (prev + 1)); + prop.notifyAll(); + } + + new File(dir, "data.txt").createNewFile(); + notify.event.offer(dir.getPath()); + + assertProperty("One path waiting", "1", "org.netbeans.io.pending"); + listener.assertEvents("No event yet", 0, 200); + + synchronized (prop) { + int prev = Integer.getInteger(prop, 0); + System.setProperty(prop, "" + (prev - 1)); + prop.notifyAll(); + } + assertProperty("Pending status is cleared", null, "org.netbeans.io.pending"); + + listener.assertEvents("One event delivered", 1, 5000); + + arr = folder.getChildren(); + assertEquals("One child", 1, arr.length); + assertEquals("data.txt", arr[0].getNameExt()); + } + + private void assertProperty(String msg, String expVal, String propName) throws InterruptedException { + String val = null; + for (int i = 0; i < 100; i++) { + val = System.getProperty(propName); + if (expVal == null && val == null) { + return; + } + if (expVal != null && expVal.equals(val)) { + return; + } + Thread.sleep(100); + } + fail(msg + " exp: " + expVal + " was: " + val); + } + + + private static final class L extends FileChangeAdapter { + private int cnt; + + @Override + public synchronized void fileDataCreated(FileEvent fe) { + cnt++; + } + + @Override + public void fileFolderCreated(FileEvent fe) { + cnt++; + } + + private synchronized void assertEvents( + String msg, int cnt, int timeOut + ) throws InterruptedException { + if (this.cnt != cnt) { + wait(timeOut); + } + assertEquals(msg, cnt, this.cnt); + } + + } + + public static final class TestNotifier extends Notifier { + final List registered = new LinkedList(); + final BlockingQueue event = new ArrayBlockingQueue(10); + + @Override + public Integer addWatch(String path) throws IOException { + int size = registered.size(); + registered.add(path); + return size; + } + + @Override + public void removeWatch(Integer key) throws IOException { + registered.set(key, null); + } + + @Override + public String nextEvent() throws IOException, InterruptedException { + return event.take(); + } + + @Override + protected void start() throws IOException { + registered.clear(); + } + } +}