Skip to content

Commit 7ea437d

Browse files
authored
Merge pull request #628 from stariy95/FIX-4.2-CAY-2876
CAY-2876 Memory leak in the ObjectStore
2 parents 2edc46f + da0256c commit 7ea437d

File tree

3 files changed

+56
-9
lines changed

3 files changed

+56
-9
lines changed

RELEASE-NOTES.txt

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Bug Fixes:
2222
CAY-2866 DefaultDataDomainFlushAction breaks on circular relationship update
2323
CAY-2868 Regression: DefaultDbRowOpSorter shouldn't sort update operations
2424
CAY-2871 QualifierTranslator breaks on a relationship with a compound FK
25+
CAY-2876 Memory leak in the ObjectStore
2526
CAY-2879 Negative number for non parameterized ObjectSelect query not processed correctly
2627

2728
----------------------------------

cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStore.java

+35-9
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
import org.apache.cayenne.reflect.PropertyVisitor;
4545
import org.apache.cayenne.reflect.ToManyProperty;
4646
import org.apache.cayenne.reflect.ToOneProperty;
47+
import org.apache.cayenne.util.SoftValueMap;
48+
import org.apache.cayenne.util.WeakValueMap;
4749

4850
import java.io.Serializable;
4951
import java.util.ArrayList;
@@ -53,7 +55,6 @@
5355
import java.util.Iterator;
5456
import java.util.List;
5557
import java.util.Map;
56-
import java.util.Set;
5758
import java.util.concurrent.ConcurrentHashMap;
5859

5960
/**
@@ -109,13 +110,23 @@ public class ObjectStore implements Serializable, SnapshotEventListener, GraphMa
109110
*/
110111
public ObjectStore(DataRowStore dataRowCache, Map<Object, Persistent> objectMap) {
111112
setDataRowCache(dataRowCache);
112-
if (objectMap != null) {
113-
this.objectMap = objectMap;
114-
}
115-
else {
113+
setObjectMap(objectMap);
114+
this.changes = new HashMap<>();
115+
}
116+
117+
/**
118+
* @since 4.2.2
119+
*/
120+
void setObjectMap(Map<Object, Persistent> objectMap) {
121+
if(objectMap == null) {
116122
throw new CayenneRuntimeException("Object map is null.");
117123
}
118-
this.changes = new HashMap<>();
124+
this.objectMap = objectMap;
125+
if(objectMap instanceof SoftValueMap) {
126+
((SoftValueMap<Object, Persistent>) objectMap).setKeyCleanupCallback(this::onObjectKeyCleanup);
127+
} else if(objectMap instanceof WeakValueMap) {
128+
((WeakValueMap<Object, Persistent>) objectMap).setKeyCleanupCallback(this::onObjectKeyCleanup);
129+
}
119130
}
120131

121132
/**
@@ -138,7 +149,7 @@ void childContextSyncStopped() {
138149
Collection<GraphDiff> getLifecycleEventInducedChanges() {
139150
return lifecycleEventInducedChanges != null
140151
? lifecycleEventInducedChanges
141-
: Collections.<GraphDiff>emptyList();
152+
: Collections.emptyList();
142153
}
143154

144155
void registerLifecycleEventInducedChange(GraphDiff diff) {
@@ -416,6 +427,9 @@ public void postprocessAfterCommit(GraphDiff parentChanges) {
416427
switch (object.getPersistenceState()) {
417428
case PersistenceState.DELETED:
418429
objectMap.remove(id);
430+
if(trackedFlattenedPaths != null) {
431+
trackedFlattenedPaths.remove(id);
432+
}
419433
object.setObjectContext(null);
420434
object.setPersistenceState(PersistenceState.TRANSIENT);
421435
break;
@@ -640,9 +654,11 @@ void processDeletedID(ObjectId nodeId) {
640654
if (dataObject == null || delegate.shouldProcessDelete(dataObject)) {
641655
objectMap.remove(nodeId);
642656
changes.remove(nodeId);
657+
if(trackedFlattenedPaths != null) {
658+
trackedFlattenedPaths.remove(nodeId);
659+
}
643660

644-
// setting DataContext to null will also set
645-
// state to transient
661+
// setting DataContext to null will also set state to transient
646662
object.setObjectContext(null);
647663

648664
if (dataObject != null) {
@@ -1049,6 +1065,16 @@ public void markFlattenedPath(ObjectId objectId, String path, ObjectId id) {
10491065
.put(path, id);
10501066
}
10511067

1068+
/**
1069+
* @param key object id that was removed from the {@link #objectMap}
1070+
* @since 4.2.2
1071+
*/
1072+
void onObjectKeyCleanup(Object key) {
1073+
if(trackedFlattenedPaths != null) {
1074+
trackedFlattenedPaths.remove(key);
1075+
}
1076+
}
1077+
10521078
// an ObjectIdQuery optimized for retrieval of multiple snapshots - it can be reset
10531079
// with the new id
10541080
final class CachedSnapshotQuery extends ObjectIdQuery {

cayenne-server/src/main/java/org/apache/cayenne/util/ReferenceMap.java

+20
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.Map;
3636
import java.util.NoSuchElementException;
3737
import java.util.Set;
38+
import java.util.function.Consumer;
3839

3940
/**
4041
* 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,
8283
*/
8384
protected transient Set<Entry<K, V>> entrySet;
8485

86+
/**
87+
* @since 4.2.2
88+
*/
89+
protected transient Consumer<K> keyCleanupCallback;
90+
8591
public ReferenceMap() {
8692
map = new HashMap<>();
8793
referenceQueue = new ReferenceQueue<>();
@@ -221,6 +227,17 @@ public Set<Entry<K, V>> entrySet() {
221227
return es;
222228
}
223229

230+
/**
231+
* Set callback that will be notified with a key on each value removal
232+
* due to the corresponding value reference cleaned up by the GC.
233+
*
234+
* @param keyCleanupCallback callback to set
235+
* @since 4.2.2
236+
*/
237+
public void setKeyCleanupCallback(Consumer<K> keyCleanupCallback) {
238+
this.keyCleanupCallback = keyCleanupCallback;
239+
}
240+
224241
/**
225242
* Cleanup all references collected by GC so far
226243
*/
@@ -247,6 +264,9 @@ protected void checkReferenceQueue() {
247264

248265
for(K keyToRemove : keysToRemove) {
249266
map.remove(keyToRemove);
267+
if(keyCleanupCallback != null) {
268+
keyCleanupCallback.accept(keyToRemove);
269+
}
250270
}
251271
}
252272

0 commit comments

Comments
 (0)