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

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

commit 7fe25cd54e90f54c4514dbb52e9bac3a4f6235b0
Author: Nikita Timofeev <stari...@gmail.com>
AuthorDate: Mon Mar 10 16:04:16 2025 +0400

    CAY-2876 Memory leak in the ObjectStore
---
 RELEASE-NOTES.txt                                  |  1 +
 .../org/apache/cayenne/access/ObjectStore.java     | 34 ++++++++++++++++++----
 .../java/org/apache/cayenne/util/ReferenceMap.java | 20 +++++++++++++
 3 files changed, 49 insertions(+), 6 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index f964462b3..52ab66e33 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -21,6 +21,7 @@ Bug Fixes:
 CAY-2701 MySQL DST-related LocalDateTime issues
 CAY-2871 QualifierTranslator breaks on a relationship with a compound FK
 CAY-2872 CayenneModeler "Documentation" link is broken
+CAY-2876 Memory leak in the ObjectStore
 CAY-2879 Negative number for non parameterized ObjectSelect query not 
processed correctly
 
 ----------------------------------
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java 
b/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java
index 0dcdc7aa1..8800fd5e8 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java
@@ -44,6 +44,8 @@ import org.apache.cayenne.reflect.ClassDescriptor;
 import org.apache.cayenne.reflect.PropertyVisitor;
 import org.apache.cayenne.reflect.ToManyProperty;
 import org.apache.cayenne.reflect.ToOneProperty;
+import org.apache.cayenne.util.SoftValueMap;
+import org.apache.cayenne.util.WeakValueMap;
 
 import java.io.Serializable;
 import java.util.ArrayList;
@@ -108,13 +110,23 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
      */
     public ObjectStore(DataRowStore dataRowCache, Map<Object, Persistent> 
objectMap) {
         setDataRowCache(dataRowCache);
-        if (objectMap != null) {
-            this.objectMap = objectMap;
-        }
-        else {
+        setObjectMap(objectMap);
+        this.changes = new HashMap<>();
+    }
+
+    /**
+     * @since 4.2.2
+     */
+    void setObjectMap(Map<Object, Persistent> objectMap) {
+        if(objectMap == null) {
             throw new CayenneRuntimeException("Object map is null.");
         }
-        this.changes = new HashMap<>();
+        this.objectMap = objectMap;
+        if(objectMap instanceof SoftValueMap) {
+            ((SoftValueMap<Object, Persistent>) 
objectMap).setKeyCleanupCallback(this::onObjectKeyCleanup);
+        } else if(objectMap instanceof WeakValueMap) {
+            ((WeakValueMap<Object, Persistent>) 
objectMap).setKeyCleanupCallback(this::onObjectKeyCleanup);
+        }
     }
 
     /**
@@ -137,7 +149,7 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
     Collection<GraphDiff> getLifecycleEventInducedChanges() {
         return lifecycleEventInducedChanges != null
                 ? lifecycleEventInducedChanges
-                : Collections.<GraphDiff>emptyList();
+                : Collections.emptyList();
     }
 
     void registerLifecycleEventInducedChange(GraphDiff diff) {
@@ -1036,6 +1048,16 @@ public class ObjectStore implements Serializable, 
SnapshotEventListener, GraphMa
                 .put(path, id);
     }
 
+    /**
+     * @param key object id that was removed from the {@link #objectMap}
+     * @since 4.2.2
+     */
+    void onObjectKeyCleanup(Object key) {
+        if(trackedFlattenedPaths != null) {
+            trackedFlattenedPaths.remove(key);
+        }
+    }
+
     // an ObjectIdQuery optimized for retrieval of multiple snapshots - it can 
be reset
     // with the new id
     static final class CachedSnapshotQuery extends ObjectIdQuery {
diff --git a/cayenne/src/main/java/org/apache/cayenne/util/ReferenceMap.java 
b/cayenne/src/main/java/org/apache/cayenne/util/ReferenceMap.java
index 7eb814923..15daf5352 100644
--- a/cayenne/src/main/java/org/apache/cayenne/util/ReferenceMap.java
+++ b/cayenne/src/main/java/org/apache/cayenne/util/ReferenceMap.java
@@ -35,6 +35,7 @@ import java.util.Iterator;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Set;
+import java.util.function.Consumer;
 
 /**
  * Map that transparently stores values as references and resolves them as 
needed.
@@ -82,6 +83,11 @@ abstract class ReferenceMap<K, V, R extends Reference<V>> 
extends AbstractMap<K,
      */
     protected transient Set<Entry<K, V>> entrySet;
 
+    /**
+     * @since 4.2.2
+     */
+    protected transient Consumer<K> keyCleanupCallback;
+
     public ReferenceMap() {
         map = new HashMap<>();
         referenceQueue = new ReferenceQueue<>();
@@ -221,6 +227,17 @@ abstract class ReferenceMap<K, V, R extends Reference<V>> 
extends AbstractMap<K,
         return es;
     }
 
+    /**
+     * Set callback that will be notified with a key on each value removal
+     * due to the corresponding value reference cleaned up by the GC.
+     *
+     * @param keyCleanupCallback callback to set
+     * @since 4.2.2
+     */
+    public void setKeyCleanupCallback(Consumer<K> keyCleanupCallback) {
+        this.keyCleanupCallback = keyCleanupCallback;
+    }
+
     /**
      * Cleanup all references collected by GC so far
      */
@@ -247,6 +264,9 @@ abstract class ReferenceMap<K, V, R extends Reference<V>> 
extends AbstractMap<K,
 
         for(K keyToRemove : keysToRemove) {
             map.remove(keyToRemove);
+            if(keyCleanupCallback != null) {
+                keyCleanupCallback.accept(keyToRemove);
+            }
         }
     }
 

Reply via email to