data race on adding/consuming ourInstances; deregister and stop LowMemoryWatcher... phpstorm/163.1034 rubymine/163.1035
authorAlexey Kudravtsev <cdr@intellij.com>
Wed, 6 Jul 2016 12:05:13 +0000 (15:05 +0300)
committerAlexey Kudravtsev <cdr@intellij.com>
Wed, 6 Jul 2016 12:05:13 +0000 (15:05 +0300)
platform/util/src/com/intellij/openapi/util/LowMemoryWatcher.java
platform/util/src/com/intellij/openapi/util/LowMemoryWatcherManager.java [new file with mode: 0644]
platform/util/src/com/intellij/util/concurrency/AppScheduledExecutorService.java

index 60f10dc3ae80b6b6f0fe5abed00aa8e2269be9da..4edf039b7545b93de60ca7568ee0ae70fa32436e 100644 (file)
@@ -17,85 +17,31 @@ package com.intellij.openapi.util;
 
 import com.intellij.openapi.Disposable;
 import com.intellij.openapi.diagnostic.Logger;
 
 import com.intellij.openapi.Disposable;
 import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.util.concurrency.AppExecutorUtil;
 import com.intellij.util.containers.WeakList;
 import com.intellij.util.containers.WeakList;
+import org.jetbrains.annotations.Contract;
 import org.jetbrains.annotations.NotNull;
 
 import org.jetbrains.annotations.NotNull;
 
-import javax.management.ListenerNotFoundException;
-import javax.management.Notification;
-import javax.management.NotificationEmitter;
-import javax.management.NotificationListener;
-import java.lang.management.ManagementFactory;
-import java.lang.management.MemoryNotificationInfo;
-import java.lang.management.MemoryPoolMXBean;
-import java.lang.management.MemoryType;
 import java.util.List;
 import java.util.List;
-import java.util.concurrent.Future;
 
 /**
  * @author Eugene Zhuravlev
  *         Date: Aug 24, 2010
  */
 public class LowMemoryWatcher {
 
 /**
  * @author Eugene Zhuravlev
  *         Date: Aug 24, 2010
  */
 public class LowMemoryWatcher {
-  private static final long MEM_THRESHOLD = 5 /*MB*/ * 1024 * 1024;
-
   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.LowMemoryWatcher");
 
   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.LowMemoryWatcher");
 
-  private static final List<LowMemoryWatcher> ourInstances = new WeakList<LowMemoryWatcher>();
-  private static Future<?> ourSubmitted;
-  private static final Runnable ourJanitor = new Runnable() {
-    @Override
-    public void run() {
-      LOG.info("Low memory signal received.");
-      try {
-        for (LowMemoryWatcher watcher : ourInstances) {
-          try {
-            watcher.myRunnable.run();
-          }
-          catch (Throwable e) {
-            LOG.info(e);
-          }
-        }
-      }
-      finally {
-        synchronized (ourJanitor) {
-          ourSubmitted = null;
-        }
-      }
-    }
-  };
-  private static final NotificationListener ourLowMemoryListener = new NotificationListener() {
-    @Override
-    public void handleNotification(Notification n, Object hb) {
-      if (MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED.equals(n.getType()) ||
-          MemoryNotificationInfo.MEMORY_COLLECTION_THRESHOLD_EXCEEDED.equals(n.getType())) {
-        synchronized (ourJanitor) {
-          if (ourSubmitted == null) {
-            ourSubmitted = AppExecutorUtil.getAppExecutorService().submit(ourJanitor);
-          }
-        }
-      }
-    }
-  };
-
+  private static final List<Runnable> ourListeners = new WeakList<Runnable>();
   private final Runnable myRunnable;
 
   private final Runnable myRunnable;
 
-  static {
-    try {
-      for (MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) {
-        if (bean.getType() == MemoryType.HEAP && bean.isUsageThresholdSupported()) {
-          long threshold = bean.getUsage().getMax() - MEM_THRESHOLD;
-          if (threshold > 0) {
-            bean.setUsageThreshold(threshold);
-            bean.setCollectionUsageThreshold(threshold);
-          }
-        }
+  static void onLowMemorySignalReceived() {
+    LOG.info("Low memory signal received.");
+    for (Runnable watcher : ourListeners) {
+      try {
+        watcher.run();
+      }
+      catch (Throwable e) {
+        LOG.info(e);
       }
       }
-      ((NotificationEmitter)ManagementFactory.getMemoryMXBean()).addNotificationListener(ourLowMemoryListener, null, null);
-    }
-    catch (Throwable e) {
-      // should not happen normally
-      LOG.info("Errors initializing LowMemoryWatcher: ", e);
     }
   }
 
     }
   }
 
@@ -103,7 +49,12 @@ public class LowMemoryWatcher {
    * Registers a runnable to run on low memory events
    * @return a LowMemoryWatcher instance holding the runnable. This instance should be kept in memory while the
    * low memory notification functionality is needed. As soon as it's garbage-collected, the runnable won't receive any further notifications.
    * Registers a runnable to run on low memory events
    * @return a LowMemoryWatcher instance holding the runnable. This instance should be kept in memory while the
    * low memory notification functionality is needed. As soon as it's garbage-collected, the runnable won't receive any further notifications.
+   * @param runnable the action which executes on low-memory condition. Can be executed:
+   *                 - in arbitrary thread
+   *                 - in unpredictable time
+   *                 - multiple copies in parallel so please make it reentrant.
    */
    */
+  @Contract(pure = true)
   public static LowMemoryWatcher register(@NotNull Runnable runnable) {
     return new LowMemoryWatcher(runnable);
   }
   public static LowMemoryWatcher register(@NotNull Runnable runnable) {
     return new LowMemoryWatcher(runnable);
   }
@@ -112,23 +63,22 @@ public class LowMemoryWatcher {
    * Registers a runnable to run on low memory events. The notifications will be issued until parentDisposable is disposed.
    */
   public static void register(@NotNull Runnable runnable, @NotNull Disposable parentDisposable) {
    * Registers a runnable to run on low memory events. The notifications will be issued until parentDisposable is disposed.
    */
   public static void register(@NotNull Runnable runnable, @NotNull Disposable parentDisposable) {
-    final Ref<LowMemoryWatcher> watcher = Ref.create(new LowMemoryWatcher(runnable));
+    final LowMemoryWatcher watcher = new LowMemoryWatcher(runnable);
     Disposer.register(parentDisposable, new Disposable() {
       @Override
       public void dispose() {
     Disposer.register(parentDisposable, new Disposable() {
       @Override
       public void dispose() {
-        watcher.get().stop();
-        watcher.set(null);
+        watcher.stop();
       }
     });
   }
 
   private LowMemoryWatcher(@NotNull Runnable runnable) {
     myRunnable = runnable;
       }
     });
   }
 
   private LowMemoryWatcher(@NotNull Runnable runnable) {
     myRunnable = runnable;
-    ourInstances.add(this);
+    ourListeners.add(runnable);
   }
 
   public void stop() {
   }
 
   public void stop() {
-    ourInstances.remove(this);
+    ourListeners.remove(myRunnable);
   }
 
   /**
   }
 
   /**
@@ -136,19 +86,7 @@ public class LowMemoryWatcher {
    * In server environments, this thread may run indefinitely and prevent the class loader from
    * being gc-ed. Thus it's necessary to invoke this method to stop that thread and let the classes be garbage-collected.
    */
    * In server environments, this thread may run indefinitely and prevent the class loader from
    * being gc-ed. Thus it's necessary to invoke this method to stop that thread and let the classes be garbage-collected.
    */
-  public static void stopAll() {
-    synchronized (ourJanitor) {
-      if (ourSubmitted != null) {
-        ourSubmitted.cancel(false);
-        ourSubmitted = null;
-      }
-    }
-    ourInstances.clear();
-    try {
-      ((NotificationEmitter)ManagementFactory.getMemoryMXBean()).removeNotificationListener(ourLowMemoryListener);
-    }
-    catch (ListenerNotFoundException e) {
-      LOG.error(e);
-    }
+  static void stopAll() {
+    ourListeners.clear();
   }
 }
   }
 }
diff --git a/platform/util/src/com/intellij/openapi/util/LowMemoryWatcherManager.java b/platform/util/src/com/intellij/openapi/util/LowMemoryWatcherManager.java
new file mode 100644 (file)
index 0000000..ca5375c
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed 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 com.intellij.openapi.util;
+
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.util.concurrency.AppExecutorUtil;
+
+import javax.management.ListenerNotFoundException;
+import javax.management.Notification;
+import javax.management.NotificationEmitter;
+import javax.management.NotificationListener;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryNotificationInfo;
+import java.lang.management.MemoryPoolMXBean;
+import java.lang.management.MemoryType;
+import java.util.concurrent.Future;
+
+public class LowMemoryWatcherManager implements Disposable {
+  private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.LowMemoryWatcherManager");
+
+  private static final long MEM_THRESHOLD = 5 /*MB*/ * 1024 * 1024;
+
+  private Future<?> mySubmitted; // guarded by ourJanitor
+  private final Runnable myJanitor = new Runnable() {
+    @Override
+    public void run() {
+      // null mySubmitted before all listeners called to avoid data race when listener added in the middle of the execution and is lost
+      // this may however cause listeners to execute more than once (potentially even in parallel)
+      synchronized (myJanitor) {
+        mySubmitted = null;
+      }
+      LowMemoryWatcher.onLowMemorySignalReceived();
+    }
+  };
+
+  public LowMemoryWatcherManager() {
+    try {
+      for (MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) {
+        if (bean.getType() == MemoryType.HEAP && bean.isUsageThresholdSupported()) {
+          long threshold = bean.getUsage().getMax() - MEM_THRESHOLD;
+          if (threshold > 0) {
+            bean.setUsageThreshold(threshold);
+            bean.setCollectionUsageThreshold(threshold);
+          }
+        }
+      }
+      ((NotificationEmitter)ManagementFactory.getMemoryMXBean()).addNotificationListener(myLowMemoryListener, null, null);
+    }
+    catch (Throwable e) {
+      // should not happen normally
+      LOG.info("Errors initializing LowMemoryWatcher: ", e);
+    }
+  }
+
+  private final NotificationListener myLowMemoryListener = new NotificationListener() {
+    @Override
+    public void handleNotification(Notification notification, Object __) {
+      if (MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED.equals(notification.getType()) ||
+          MemoryNotificationInfo.MEMORY_COLLECTION_THRESHOLD_EXCEEDED.equals(notification.getType())) {
+        synchronized (myJanitor) {
+          if (mySubmitted == null) {
+            mySubmitted = AppExecutorUtil.getAppExecutorService().submit(myJanitor);
+          }
+        }
+      }
+    }
+  };
+
+  @Override
+  public void dispose() {
+    try {
+      ((NotificationEmitter)ManagementFactory.getMemoryMXBean()).removeNotificationListener(myLowMemoryListener);
+    }
+    catch (ListenerNotFoundException e) {
+      LOG.error(e);
+    }
+    synchronized (myJanitor) {
+      if (mySubmitted != null) {
+        mySubmitted.cancel(false);
+        mySubmitted = null;
+      }
+    }
+
+    LowMemoryWatcher.stopAll();
+  }
+}
index be08014f1e38c3aeb0469a76147d86693cec19ad..fed13f11e2f66becb641e94d5326b4cb17f4ac5f 100644 (file)
@@ -16,6 +16,8 @@
 package com.intellij.util.concurrency;
 
 import com.intellij.openapi.diagnostic.Logger;
 package com.intellij.util.concurrency;
 
 import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.LowMemoryWatcherManager;
 import com.intellij.util.Consumer;
 import com.intellij.util.IncorrectOperationException;
 import com.intellij.util.containers.ContainerUtil;
 import com.intellij.util.Consumer;
 import com.intellij.util.IncorrectOperationException;
 import com.intellij.util.containers.ContainerUtil;
@@ -35,6 +37,7 @@ public class AppScheduledExecutorService extends SchedulingWrapper {
   private static final Logger LOG = Logger.getInstance("#org.jetbrains.ide.PooledThreadExecutor");
   static final String POOLED_THREAD_PREFIX = "ApplicationImpl pooled thread ";
   @NotNull private final String myName;
   private static final Logger LOG = Logger.getInstance("#org.jetbrains.ide.PooledThreadExecutor");
   static final String POOLED_THREAD_PREFIX = "ApplicationImpl pooled thread ";
   @NotNull private final String myName;
+  private final LowMemoryWatcherManager myLowMemoryWatcherManager = new LowMemoryWatcherManager();
   private Consumer<Thread> newThreadListener;
   private final AtomicInteger counter = new AtomicInteger();
 
   private Consumer<Thread> newThreadListener;
   private final AtomicInteger counter = new AtomicInteger();
 
@@ -100,6 +103,8 @@ public class AppScheduledExecutorService extends SchedulingWrapper {
   }
 
   public void shutdownAppScheduledExecutorService() {
   }
 
   public void shutdownAppScheduledExecutorService() {
+    // LowMemoryWatcher starts background threads so stop it now to avoid RejectedExecutionException
+    Disposer.dispose(myLowMemoryWatcherManager);
     delayQueue.shutdown(); // shutdown delay queue first to avoid rejected execution exceptions in Alarm
     doShutdown();
   }
     delayQueue.shutdown(); // shutdown delay queue first to avoid rejected execution exceptions in Alarm
     doShutdown();
   }