]> source.dussan.org Git - vaadin-framework.git/commitdiff
Protect CurrentInstance instances from garbage collection (#12509)
authorLeif Åstrand <leif@vaadin.com>
Thu, 5 Sep 2013 07:11:07 +0000 (10:11 +0300)
committerVaadin Code Review <review@vaadin.com>
Thu, 5 Sep 2013 13:34:10 +0000 (13:34 +0000)
Change-Id: I9ec103a1a42d8888d6f680f477393807223740cf

server/src/com/vaadin/util/CurrentInstance.java
server/src/com/vaadin/util/WeakValueMap.java [deleted file]
uitest/src/com/vaadin/tests/components/ui/CurrentUiRetained.html [new file with mode: 0644]
uitest/src/com/vaadin/tests/components/ui/CurrentUiRetained.java [new file with mode: 0644]

index a1c543117dac00c4aee08b0d0e3af157447858e4..3320a043668b55d4d4ecf3840f9b6436a0f268a7 100644 (file)
 package com.vaadin.util;
 
 import java.io.Serializable;
+import java.lang.ref.WeakReference;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import com.vaadin.server.VaadinRequest;
 import com.vaadin.server.VaadinResponse;
@@ -36,6 +40,10 @@ import com.vaadin.ui.UI;
  * when {@link VaadinSession#access(Runnable)} or {@link UI#access(Runnable)} is
  * used.
  * <p>
+ * Please note that the instances are stored using {@link WeakReference}. This
+ * means that the a current instance value may suddenly disappear if there a no
+ * other references to the object.
+ * <p>
  * Currently the framework uses the following instances:
  * </p>
  * <p>
@@ -49,7 +57,7 @@ import com.vaadin.ui.UI;
  * @since 7.0.0
  */
 public class CurrentInstance implements Serializable {
-    private final Object instance;
+    private final WeakReference<Object> instance;
     private final boolean inheritable;
 
     private static InheritableThreadLocal<Map<Class<?>, CurrentInstance>> instances = new InheritableThreadLocal<Map<Class<?>, CurrentInstance>>() {
@@ -60,7 +68,7 @@ public class CurrentInstance implements Serializable {
                 return null;
             }
 
-            Map<Class<?>, CurrentInstance> value = new WeakValueMap<Class<?>, CurrentInstance>();
+            Map<Class<?>, CurrentInstance> value = new HashMap<Class<?>, CurrentInstance>();
 
             // Copy all inheritable values to child map
             for (Entry<Class<?>, CurrentInstance> e : parentValue.entrySet()) {
@@ -74,7 +82,7 @@ public class CurrentInstance implements Serializable {
     };
 
     private CurrentInstance(Object instance, boolean inheritable) {
-        this.instance = instance;
+        this.instance = new WeakReference<Object>(instance);
         this.inheritable = inheritable;
     }
 
@@ -93,12 +101,49 @@ public class CurrentInstance implements Serializable {
         }
         CurrentInstance currentInstance = map.get(type);
         if (currentInstance != null) {
-            return type.cast(currentInstance.instance);
+            Object value = currentInstance.instance.get();
+            if (value == null) {
+                /*
+                 * This is believed to never actually happen since the
+                 * ThreadLocal should only outlive the referenced object on
+                 * threads that are not doing anything related to Vaadin, which
+                 * should thus never invoke CurrentInstance.get().
+                 * 
+                 * At this point, there might also be other values that have
+                 * been collected, so we'll scan the entire map and remove stale
+                 * CurrentInstance objects. Using a ReferenceQueue could make
+                 * this assumingly rare case slightly more efficient, but would
+                 * significantly increase the complexity of the code for
+                 * maintaining a separate ReferenceQueue for each Thread.
+                 */
+                removeStaleInstances(map);
+
+                if (map.isEmpty()) {
+                    instances.remove();
+                }
+
+                return null;
+            }
+            return type.cast(value);
         } else {
             return null;
         }
     }
 
+    private static void removeStaleInstances(Map<Class<?>, CurrentInstance> map) {
+        for (Iterator<Entry<Class<?>, CurrentInstance>> iterator = map
+                .entrySet().iterator(); iterator.hasNext();) {
+            Entry<Class<?>, CurrentInstance> entry = iterator.next();
+            Object instance = entry.getValue().instance.get();
+            if (instance == null) {
+                iterator.remove();
+                getLogger().log(Level.WARNING,
+                        "CurrentInstance for {0} has been garbage collected.",
+                        entry.getKey());
+            }
+        }
+    }
+
     /**
      * Sets the current instance of the given type.
      * 
@@ -183,9 +228,19 @@ public class CurrentInstance implements Serializable {
      *            A Class -> CurrentInstance map to set as current instances
      */
     public static void restoreInstances(Map<Class<?>, CurrentInstance> old) {
+        boolean removeStale = false;
         for (Class c : old.keySet()) {
             CurrentInstance ci = old.get(c);
-            set(c, ci.instance, ci.inheritable);
+            Object v = ci.instance.get();
+            if (v == null) {
+                removeStale = true;
+            } else {
+                set(c, v, ci.inheritable);
+            }
+        }
+
+        if (removeStale) {
+            removeStaleInstances(old);
         }
     }
 
@@ -207,12 +262,21 @@ public class CurrentInstance implements Serializable {
             return Collections.emptyMap();
         } else {
             Map<Class<?>, CurrentInstance> copy = new HashMap<Class<?>, CurrentInstance>();
+            boolean removeStale = false;
             for (Class<?> c : map.keySet()) {
                 CurrentInstance ci = map.get(c);
-                if (ci.inheritable || !onlyInheritable) {
+                if (ci.instance.isEnqueued()) {
+                    removeStale = true;
+                } else if (ci.inheritable || !onlyInheritable) {
                     copy.put(c, ci);
                 }
             }
+            if (removeStale) {
+                removeStaleInstances(map);
+                if (map.isEmpty()) {
+                    instances.remove();
+                }
+            }
             return copy;
         }
     }
@@ -266,4 +330,8 @@ public class CurrentInstance implements Serializable {
 
         return old;
     }
+
+    private static Logger getLogger() {
+        return Logger.getLogger(CurrentInstance.class.getName());
+    }
 }
diff --git a/server/src/com/vaadin/util/WeakValueMap.java b/server/src/com/vaadin/util/WeakValueMap.java
deleted file mode 100644 (file)
index 1134594..0000000
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * Copyright 2000-2013 Vaadin Ltd.
- *
- * 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.vaadin.util;
-
-import java.lang.ref.Reference;
-import java.lang.ref.ReferenceQueue;
-import java.lang.ref.WeakReference;
-import java.util.AbstractMap;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * A Map holding weak references to its values. It is internally backed by a
- * normal HashMap and all values are stored as WeakReferences. Garbage collected
- * entries are removed when touched.
- * <p>
- * <em>Note</em> this class is not serializable.
- * 
- * @author Vaadin Ltd
- * @since 7.1.4
- */
-public class WeakValueMap<K, V> implements Map<K, V> {
-
-    /**
-     * This class holds a weak reference to the value and a strong reference to
-     * the key for efficient removal of stale values.
-     */
-    private static class WeakValueReference<K, V> extends WeakReference<V> {
-        private final K key;
-
-        WeakValueReference(K key, V value, ReferenceQueue<V> refQueue) {
-            super(value, refQueue);
-            this.key = key;
-        }
-
-        K getKey() {
-            return key;
-        }
-    }
-
-    private final HashMap<K, WeakValueReference<K, V>> backingMap;
-    private final ReferenceQueue<V> refQueue;
-
-    /**
-     * Constructs a new WeakValueMap, where all values are stored as weak
-     * references.
-     */
-    public WeakValueMap() {
-        backingMap = new HashMap<K, WeakValueReference<K, V>>();
-        refQueue = new ReferenceQueue<V>();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public V put(K key, V value) {
-        if (key == null) {
-            throw new NullPointerException("key cannot be null");
-        }
-        if (value == null) {
-            throw new NullPointerException("value cannot be null");
-        }
-        removeStaleEntries();
-        backingMap.put(key, new WeakValueReference<K, V>(key, value, refQueue));
-        return value;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public V remove(Object o) {
-        removeStaleEntries();
-        WeakReference<V> value = backingMap.remove(o);
-        return value == null ? null : value.get();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void putAll(Map<? extends K, ? extends V> map) {
-        if (map != null) {
-            for (Map.Entry<? extends K, ? extends V> entry : map.entrySet()) {
-                put(entry.getKey(), entry.getValue());
-            }
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void clear() {
-        backingMap.clear();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public Set<K> keySet() {
-        removeStaleEntries();
-        return backingMap.keySet();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public V get(Object o) {
-        removeStaleEntries();
-        WeakReference<V> weakValue = backingMap.get(o);
-        if (weakValue != null) {
-            return weakValue.get();
-        }
-        return null;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public int size() {
-        removeStaleEntries();
-        return backingMap.size();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public boolean isEmpty() {
-        removeStaleEntries();
-        return backingMap.isEmpty();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public boolean containsKey(Object o) {
-        removeStaleEntries();
-        return backingMap.containsKey(o);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public boolean containsValue(Object o) {
-        removeStaleEntries();
-        for (V value : values()) {
-            if (o.equals(value)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public Collection<V> values() {
-        removeStaleEntries();
-        Collection<V> values = new HashSet<V>();
-        for (WeakReference<V> weakValue : backingMap.values()) {
-            V value = weakValue.get();
-            if (value != null) {
-                // null values have been GC'd, which may happen long before
-                // anything is enqueued in the ReferenceQueue.
-                values.add(value);
-            }
-        }
-        return values;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public Set<Entry<K, V>> entrySet() {
-        removeStaleEntries();
-        Set<Entry<K, V>> entrySet = new HashSet<Entry<K, V>>();
-        for (Entry<K, WeakValueReference<K, V>> entry : backingMap.entrySet()) {
-            V value = entry.getValue().get();
-            if (value != null) {
-                // null values have been GC'd, which may happen long before
-                // anything is enqueued in the ReferenceQueue.
-                entrySet.add(new AbstractMap.SimpleEntry<K, V>(entry.getKey(),
-                        value));
-            }
-        }
-        return entrySet;
-    }
-
-    /**
-     * Cleans up stale entries by polling the ReferenceQueue.
-     * <p>
-     * Depending on the GC implementation and strategy, the ReferenceQueue is
-     * not necessarily notified immediately when a reference is garbage
-     * collected, but it will eventually be.
-     */
-    private void removeStaleEntries() {
-        Reference<? extends V> ref;
-        while ((ref = refQueue.poll()) != null) {
-            Object key = ((WeakValueReference<?, ?>) ref).getKey();
-            backingMap.remove(key);
-        }
-    }
-}
diff --git a/uitest/src/com/vaadin/tests/components/ui/CurrentUiRetained.html b/uitest/src/com/vaadin/tests/components/ui/CurrentUiRetained.html
new file mode 100644 (file)
index 0000000..fe030f2
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<link rel="selenium.base" href="" />
+<title>New Test</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr><td rowspan="1" colspan="3">New Test</td></tr>
+</thead><tbody>
+<tr>
+       <td>open</td>
+       <td>/run/com.vaadin.tests.components.ui.CurrentUiRetained?restartApplication</td>
+       <td></td>
+</tr>
+<tr>
+       <td>pause</td>
+       <td>2000</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>vaadin=runcomvaadintestscomponentsuiCurrentUiRetained::/VVerticalLayout[0]/Slot[2]/VVerticalLayout[0]/Slot[0]/VButton[0]/domChild[0]/domChild[0]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiCurrentUiRetained::PID_SLog_row_3</td>
+       <td>1. Correct UI.getCurrent before GC: true</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiCurrentUiRetained::PID_SLog_row_2</td>
+       <td>2. Correct UI.getCurrent after GC: true</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiCurrentUiRetained::PID_SLog_row_1</td>
+       <td>3. GC probe available before GC: true</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiCurrentUiRetained::PID_SLog_row_0</td>
+       <td>4. GC probe available after GC: false</td>
+</tr>
+
+</tbody></table>
+</body>
+</html>
diff --git a/uitest/src/com/vaadin/tests/components/ui/CurrentUiRetained.java b/uitest/src/com/vaadin/tests/components/ui/CurrentUiRetained.java
new file mode 100644 (file)
index 0000000..b0127e3
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * 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.vaadin.tests.components.ui;
+
+import java.util.ArrayList;
+
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUIWithLog;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.UI;
+import com.vaadin.util.CurrentInstance;
+
+public class CurrentUiRetained extends AbstractTestUIWithLog {
+    public static class GcProbe {
+
+    }
+
+    @Override
+    protected void setup(VaadinRequest request) {
+        final ArrayList<UI> uiLog = new ArrayList<UI>();
+        final ArrayList<Boolean> probeLog = new ArrayList<Boolean>();
+
+        final Thread thread = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    uiLog.add(UI.getCurrent());
+
+                    GcProbe gcProbe = new GcProbe();
+                    CurrentInstance.set(GcProbe.class, gcProbe);
+                    probeLog.add(CurrentInstance.get(GcProbe.class) != null);
+                    gcProbe = null;
+
+                    Thread.sleep(500l);
+                    System.gc();
+                    Thread.sleep(500l);
+
+                    probeLog.add(CurrentInstance.get(GcProbe.class) != null);
+                    uiLog.add(UI.getCurrent());
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }
+        });
+        thread.start();
+
+        addComponent(new Button("Show result", new Button.ClickListener() {
+            @Override
+            public void buttonClick(ClickEvent event) {
+                try {
+                    thread.join();
+
+                    log("Correct UI.getCurrent before GC: "
+                            + (uiLog.get(0) == CurrentUiRetained.this));
+                    log("Correct UI.getCurrent after GC: "
+                            + (uiLog.get(1) == CurrentUiRetained.this));
+
+                    log("GC probe available before GC: " + probeLog.get(0));
+                    log("GC probe available after GC: " + probeLog.get(1));
+
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        }));
+    }
+
+    @Override
+    protected String getTestDescription() {
+        return "Tests that garbage collection removes stale CurrentInstance values while retaining values not collected.";
+    }
+
+    @Override
+    protected Integer getTicketNumber() {
+        return Integer.valueOf(12509);
+    }
+
+}