Skip to content

Commit 4ce1c16

Browse files
committed
CacheKey NPE and DeadLock
Signed-off-by: Radek Felcman <[email protected]>
1 parent af60e4e commit 4ce1c16

File tree

13 files changed

+4122
-60
lines changed

13 files changed

+4122
-60
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
* -----------------------------------------------------------------------------
3+
* manually
4+
* Removed text manually
5+
* Removed text manually
6+
* -----------------------------------------------------------------------------
7+
*/
8+
package org.eclipse.persistence.testing.tests.junit.helper;
9+
10+
import java.util.List;
11+
12+
import org.eclipse.persistence.internal.helper.ConcurrencyManager;
13+
import org.eclipse.persistence.internal.helper.ReadLockManager;
14+
import org.eclipse.persistence.internal.helper.type.ReadLockAcquisitionMetadata;
15+
import org.eclipse.persistence.internal.identitymaps.CacheKey;
16+
import org.junit.Assert;
17+
import org.junit.Before;
18+
import org.junit.Test;
19+
20+
/**
21+
* Unit test for the class {@link org.eclipse.persistence.internal.helper.ReadLockManager}
22+
*/
23+
public class ReadLockAcquisitionMetadataTest {
24+
25+
// Inner class
26+
/**
27+
* Unit test extension class to allows to create some hacky methods to access
28+
* and manipulate the state of the read lock manager.
29+
* Such as adding NULL records into the state.
30+
*/
31+
public static class ReadLockManagerExtension extends ReadLockManager{
32+
33+
/**
34+
* This is a dummy method hack our state maps with null entries.
35+
* Our expectation is that this should never happen in real life
36+
* but we have seen null pointer exceptions that seem to indicate that this is factuallz posisbly to happen
37+
* although we have understoor the underlying reason for the null entries.
38+
*
39+
* With this method we simulate corrupting our read data structures with null entries.
40+
*/
41+
public void hackReadLockManagerToBeCorruptedWithNullRecordsItsState() {
42+
ConcurrencyManager cacheKeyNull = null;
43+
readLocks.add(cacheKeyNull);
44+
final Thread currentThread = Thread.currentThread();
45+
final long currentThreadId = currentThread.getId();
46+
ReadLockAcquisitionMetadata readLockAcquisitionMetadataNull = null;
47+
List<ReadLockAcquisitionMetadata> readLocksAcquiredDuringCurrentThreadList = mapThreadToReadLockAcquisitionMetadata.get(currentThreadId);
48+
readLocksAcquiredDuringCurrentThreadList.add(readLockAcquisitionMetadataNull);
49+
50+
}
51+
52+
53+
}
54+
// helpver variables
55+
56+
final ConcurrencyManager cacheKeyA = new CacheKey("cacheKeyA");
57+
final ConcurrencyManager cacheKeyB = new CacheKey("cacheKeyB");
58+
59+
60+
@Before
61+
public void before() {
62+
63+
}
64+
65+
66+
@Test
67+
public void normalHappyPathLogicAddingAndRemovingMetadataIntoTheReadLockManager() {
68+
// SETUP:
69+
// basic variable initialization
70+
ReadLockManagerExtension testee = new ReadLockManagerExtension();
71+
72+
73+
// EXECUTE 1 - Add cache key A
74+
testee.addReadLock(cacheKeyA);
75+
76+
// VERIFY 1
77+
78+
Assert.assertEquals(1, testee.getReadLocks().size());
79+
Assert.assertTrue(testee.getReadLocks().contains(cacheKeyA));
80+
Assert.assertFalse(testee.getReadLocks().contains(cacheKeyB));
81+
82+
Assert.assertEquals(1, testee.getMapThreadToReadLockAcquisitionMetadata().size());
83+
List<ReadLockAcquisitionMetadata> cacheKeyMetadataForCurrentThread =testee.getMapThreadToReadLockAcquisitionMetadata().get(Thread.currentThread().getId());
84+
Assert.assertEquals(1, cacheKeyMetadataForCurrentThread.size());
85+
86+
Assert.assertTrue(cacheKeyA == cacheKeyMetadataForCurrentThread.get(0).getCacheKeyWhoseNumberOfReadersThreadIsIncrementing());
87+
Assert.assertFalse(cacheKeyB == cacheKeyMetadataForCurrentThread.get(0).getCacheKeyWhoseNumberOfReadersThreadIsIncrementing());
88+
89+
90+
// EXECUTE 2 - Add cache key B
91+
testee.addReadLock(cacheKeyB);
92+
93+
// VERIFY 2
94+
95+
Assert.assertEquals(2, testee.getReadLocks().size());
96+
Assert.assertTrue(testee.getReadLocks().contains(cacheKeyA));
97+
Assert.assertTrue(testee.getReadLocks().contains(cacheKeyB));
98+
99+
Assert.assertEquals(1, testee.getMapThreadToReadLockAcquisitionMetadata().size());
100+
cacheKeyMetadataForCurrentThread =testee.getMapThreadToReadLockAcquisitionMetadata().get(Thread.currentThread().getId());
101+
Assert.assertEquals(2, cacheKeyMetadataForCurrentThread.size());
102+
103+
// note: when we are adding, we are adding the entries to the HEAD of the list
104+
Assert.assertTrue(cacheKeyB == cacheKeyMetadataForCurrentThread.get(0).getCacheKeyWhoseNumberOfReadersThreadIsIncrementing());
105+
Assert.assertTrue(cacheKeyA == cacheKeyMetadataForCurrentThread.get(1).getCacheKeyWhoseNumberOfReadersThreadIsIncrementing());
106+
107+
108+
// EXECUTE 3 - Remove cache keys
109+
testee.removeReadLock(cacheKeyA);
110+
111+
// VERIFY 3
112+
Assert.assertEquals(1, testee.getReadLocks().size());
113+
Assert.assertFalse(testee.getReadLocks().contains(cacheKeyA));
114+
Assert.assertTrue(testee.getReadLocks().contains(cacheKeyB));
115+
116+
Assert.assertEquals(1, testee.getMapThreadToReadLockAcquisitionMetadata().size());
117+
cacheKeyMetadataForCurrentThread =testee.getMapThreadToReadLockAcquisitionMetadata().get(Thread.currentThread().getId());
118+
Assert.assertEquals(1, cacheKeyMetadataForCurrentThread.size());
119+
120+
Assert.assertTrue(cacheKeyB == cacheKeyMetadataForCurrentThread.get(0).getCacheKeyWhoseNumberOfReadersThreadIsIncrementing());
121+
Assert.assertFalse(cacheKeyA == cacheKeyMetadataForCurrentThread.get(0).getCacheKeyWhoseNumberOfReadersThreadIsIncrementing());
122+
123+
// EXECUTE 4 - Remove cache keys
124+
testee.removeReadLock(cacheKeyB);
125+
126+
// VERIFY 4
127+
Assert.assertEquals(0, testee.getReadLocks().size());
128+
Assert.assertFalse(testee.getReadLocks().contains(cacheKeyA));
129+
Assert.assertFalse(testee.getReadLocks().contains(cacheKeyB));
130+
Assert.assertEquals(0, testee.getMapThreadToReadLockAcquisitionMetadata().size());
131+
132+
133+
}
134+
135+
@Test
136+
public void testAddNullReadCacheKeyDoesNothing() {
137+
// SETUP:
138+
// basic variable initialization
139+
ReadLockManagerExtension testee = new ReadLockManagerExtension();
140+
ConcurrencyManager cacheKeyNull = null;
141+
142+
// EXECUTE
143+
// try to add a null cache key to the map
144+
testee.addReadLock(cacheKeyNull);
145+
146+
// VERIFY
147+
Assert.assertEquals(0, testee.getReadLocks().size());
148+
Assert.assertEquals(0, testee.getMapThreadToReadLockAcquisitionMetadata().size());
149+
}
150+
151+
/**
152+
* The purpose of this unit test is to make sure that if for some unknown reason
153+
* we ever end up adding NULL as metadata to either the READ Lock manager
154+
* or to the VECTOR of read locks
155+
* that we can self heald and remove them from the map automatically.
156+
*
157+
*/
158+
@Test
159+
public void testRemoveWhen_mapThreadToReadLockAcquisitionMetadata_containsNull() {
160+
// SETUP:
161+
// let us start by adding some entrires to the read lock manager
162+
ReadLockManagerExtension testee = new ReadLockManagerExtension();
163+
testee.addReadLock(cacheKeyA);
164+
testee.addReadLock(cacheKeyB);
165+
Assert.assertEquals(2, testee.getReadLocks().size());
166+
Assert.assertEquals(1, testee.getMapThreadToReadLockAcquisitionMetadata().size());
167+
List<ReadLockAcquisitionMetadata> cacheKeyMetadataForCurrentThread =testee.getMapThreadToReadLockAcquisitionMetadata().get(Thread.currentThread().getId());
168+
Assert.assertEquals(2, cacheKeyMetadataForCurrentThread.size());
169+
170+
// SETUP:
171+
// now we are going to hack our state to put in here null entires both in the read locks map and in the list of metadata
172+
// Validate that that the maps are now properly hacked
173+
testee.hackReadLockManagerToBeCorruptedWithNullRecordsItsState();
174+
Assert.assertEquals(3, testee.getReadLocks().size());
175+
Assert.assertTrue( testee.getReadLocks().contains(null));
176+
cacheKeyMetadataForCurrentThread =testee.getMapThreadToReadLockAcquisitionMetadata().get(Thread.currentThread().getId());
177+
Assert.assertEquals(3, cacheKeyMetadataForCurrentThread.size());
178+
Assert.assertTrue( cacheKeyMetadataForCurrentThread.contains(null));
179+
180+
// EXECUTE
181+
// Now we will try to REMOVE from the read lock manager a cache key that does not actually exist in the map
182+
// this MUST have the side effect of causing the code to loop over ALL OF THE read lock manager metadata and spot that we
183+
// have NULL records in the metadata array
184+
// in so doing the code should self-heal and get rid of all that garbage
185+
ConcurrencyManager cacheKeyNotExistingInTheReadLockManagerToCallFullLoopOverData = new CacheKey("cacheKeyNotExistingInTheReadLockManagerToCallFullLoopOverData");
186+
testee.removeReadLock(cacheKeyNotExistingInTheReadLockManagerToCallFullLoopOverData);
187+
188+
// VERIFY that our code self healeded
189+
Assert.assertEquals(2, testee.getReadLocks().size());
190+
Assert.assertFalse( testee.getReadLocks().contains(null));
191+
cacheKeyMetadataForCurrentThread =testee.getMapThreadToReadLockAcquisitionMetadata().get(Thread.currentThread().getId());
192+
Assert.assertEquals(2, cacheKeyMetadataForCurrentThread.size());
193+
Assert.assertFalse( cacheKeyMetadataForCurrentThread.contains(null));
194+
195+
}
196+
}

foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/config/PersistenceUnitProperties.java

+45-1
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@
5555

5656
import java.io.File;
5757
import java.sql.DriverManager;
58+
import java.util.Arrays;
5859
import java.util.Collections;
5960
import java.util.HashMap;
6061
import java.util.HashSet;
62+
import java.util.List;
6163
import java.util.Map;
6264
import java.util.Set;
6365
import java.util.concurrent.TimeUnit;
@@ -3837,7 +3839,7 @@ public class PersistenceUnitProperties {
38373839
* An NoSQL datasource is a non-relational datasource such as a legacy database, NoSQL database,
38383840
* XML database, transactional and messaging systems, or ERP systems.
38393841
*
3840-
* @see javax.resource.cci.ConnectionFactory
3842+
* {@code javax.resource.cci.ConnectionFactory}
38413843
*/
38423844
public static final String NOSQL_CONNECTION_FACTORY = "eclipselink.nosql.connection-factory";
38433845

@@ -4051,6 +4053,48 @@ public class PersistenceUnitProperties {
40514053
*/
40524054
public static final String CONCURRENCY_MANAGER_ALLOW_INTERRUPTED_EXCEPTION = "eclipselink.concurrency.manager.allow.interruptedexception";
40534055

4056+
4057+
/**
4058+
* This is the persistence.xml property that tells us how the
4059+
* org.eclipse.persistence.internal.sessions.AbstractSession.getCacheKeyFromTargetSessionForMergeScenarioMergeManagerNotNullAndCacheKeyOriginalStillNull(CacheKey, Object, ObjectBuilder, ClassDescriptor, MergeManager)
4060+
* should behave.
4061+
*/
4062+
public static final String CONCURRENCY_MANAGER_ALLOW_ABSTRACT_SESSION_GET_CACHE_KEY_FOR_MERGE_MANAGER_MODE_OF_OPERATION = "eclipselink.concurrency.manager.allow.abstractsession.getCacheKeyFromTargetSessionForMerge.modeofoperation";
4063+
4064+
4065+
//TODO RFELCMAN enum - begin?
4066+
/**
4067+
* This should be the worst possible option. This is the one that gives us the issue:
4068+
* https://github.com/eclipse-ee4j/eclipselink/issues/2094.
4069+
*/
4070+
public static final String CONCURRENCY_MANAGER_ALLOW_ABSTRACT_SESSION_GET_CACHE_KEY_FOR_MERGE_MANAGER_MODE_OF_OPERATION_OPTION_BACKWARDS_COMPATIBILITY = "backwards-compatibility";
4071+
/**
4072+
* This is far from an ideal approach but already better than the old backwards-compatibility approach.
4073+
* In this code path what will happen is that our merge manger thread will be doing a loop hoping for a cache key
4074+
* acquired by some other thread releases the cache key.
4075+
* If that never happens then our merge manager will blow up.
4076+
* The assumption being that it might be the holder of cache keys that are stopping the processes from going forward.
4077+
* That is what we saw in the issue https://github.com/eclipse-ee4j/eclipselink/issues/2094
4078+
* where our merge manager thread was hold several write lock keys that were making it impossible for other threads doing object building
4079+
* to finish their work.
4080+
*/
4081+
public static final String CONCURRENCY_MANAGER_ALLOW_ABSTRACT_SESSION_GET_CACHE_KEY_FOR_MERGE_MANAGER_MODE_OF_OPERATION_OPTION_WAIT_LOOP_WITH_TIME_LIMIT = "wait-loop-with-time-limit";
4082+
//TODO RFELCMAN enum - end?
4083+
4084+
4085+
public static final String CONCURRENCY_MANAGER_ALLOW_ABSTRACT_SESSION_GET_CACHE_KEY_FOR_MERGE_MANAGER_MODE_OF_OPERATION_OPTION_DEFAULT = CONCURRENCY_MANAGER_ALLOW_ABSTRACT_SESSION_GET_CACHE_KEY_FOR_MERGE_MANAGER_MODE_OF_OPERATION_OPTION_WAIT_LOOP_WITH_TIME_LIMIT;
4086+
4087+
/**
4088+
* As the supported options for the {@link org.eclipse.persistence.config.PersistenceUnitProperties.CONCURRENCY_MANAGER_ALLOW_ABSTRACT_SESSION_GET_CACHE_KEY_FOR_MERGE_MANAGER_MODE_OF_OPERATION}
4089+
* persistence unit property.
4090+
*/
4091+
public static final List<String> CONCURRENCY_MANAGER_ALLOW_ABSTRACT_SESSION_GET_CACHE_KEY_FOR_MERGE_MANAGER_MODE_OF_OPERATION_LIST_OF_ALL_OPTIONS = Collections.unmodifiableList( Arrays.asList(
4092+
PersistenceUnitProperties.CONCURRENCY_MANAGER_ALLOW_ABSTRACT_SESSION_GET_CACHE_KEY_FOR_MERGE_MANAGER_MODE_OF_OPERATION_OPTION_BACKWARDS_COMPATIBILITY
4093+
, PersistenceUnitProperties.CONCURRENCY_MANAGER_ALLOW_ABSTRACT_SESSION_GET_CACHE_KEY_FOR_MERGE_MANAGER_MODE_OF_OPERATION_OPTION_WAIT_LOOP_WITH_TIME_LIMIT
4094+
));
4095+
4096+
4097+
40544098
/**
40554099
* <p>
40564100
* This property control (enable/disable) if <code>ConcurrencyException</code> fired when dead-lock diagnostic is enabled.

foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/internal/helper/ConcurrencyManager.java

+39-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,17 @@
1818
import java.io.Serializable;
1919
import java.io.StringWriter;
2020
import java.security.AccessController;
21-
import java.util.*;
21+
import java.util.ArrayList;
22+
import java.util.Arrays;
23+
import java.util.Date;
24+
import java.util.Enumeration;
25+
import java.util.HashMap;
26+
import java.util.HashSet;
27+
import java.util.IdentityHashMap;
28+
import java.util.List;
29+
import java.util.Map;
30+
import java.util.Set;
31+
import java.util.Vector;
2232
import java.util.concurrent.ConcurrentHashMap;
2333
import java.util.concurrent.TimeUnit;
2434
import java.util.concurrent.atomic.AtomicInteger;
@@ -853,7 +863,7 @@ public void releaseAllLocksAcquiredByThread(DeferredLockManager lockManager) {
853863
* @return Never null if the read lock manager does not yet exist for the current thread. otherwise its read log
854864
* manager is returned.
855865
*/
856-
protected static ReadLockManager getReadLockManager(Thread thread) {
866+
public static ReadLockManager getReadLockManager(Thread thread) {
857867
Map<Thread, ReadLockManager> readLockManagers = getReadLockManagers();
858868
return readLockManagers.get(thread);
859869
}
@@ -1107,4 +1117,31 @@ public Lock getInstanceLock() {
11071117
public Condition getInstanceLockCondition() {
11081118
return this.instanceLockCondition;
11091119
}
1120+
1121+
/**
1122+
* We check if cache keys is currently being owned for writing and if that owning thread happens to be the current thread doing the check.
1123+
* @return FALSE means either the thread is currently not owned by anybody for writing purposes. Or otherwise if is owned by some thread
1124+
* but the thread is not the current thread. TRUE is returned if and only if the cache key is being owned by some thread
1125+
* and that thread is not the current thread, it is some other competing thread.
1126+
*/
1127+
public synchronized boolean isAcquiredForWritingAndOwneddByADifferentThread() {
1128+
// (a) We start by using the traditional acquire implementation to check if cache key is acquired
1129+
// if the output says false we immediately return false
1130+
if(!this.isAcquired()) {
1131+
return false;
1132+
}
1133+
1134+
// (b) If the active thread is not set then the cache keys is not acquired for writing by anybody
1135+
if(this.activeThread == null) {
1136+
return false;
1137+
}
1138+
1139+
1140+
1141+
// (c) some thread is acquiring the cache key
1142+
// return need to check if it is a different thread than ours
1143+
Thread currentThread = Thread.currentThread();
1144+
return this.activeThread != currentThread;
1145+
1146+
}
11101147
}

0 commit comments

Comments
 (0)