Merge lp:~pbeaman/akiban-persistit/fix_classIndex into lp:akiban-persistit

Proposed by Peter Beaman
Status: Merged
Approved by: Peter Beaman
Approved revision: 340
Merged at revision: 337
Proposed branch: lp:~pbeaman/akiban-persistit/fix_classIndex
Merge into: lp:akiban-persistit
Diff against target: 767 lines (+407/-111)
5 files modified
src/main/java/com/persistit/ClassIndex.java (+153/-89)
src/main/java/com/persistit/ClassInfo.java (+5/-1)
src/main/java/com/persistit/Persistit.java (+7/-8)
src/test/java/com/persistit/ClassIndexTest.java (+213/-13)
src/test/java/com/persistit/MockSerializableObject.java (+29/-0)
To merge this branch: bzr merge lp:~pbeaman/akiban-persistit/fix_classIndex
Reviewer Review Type Date Requested Status
Peter Beaman Approve
Akiban Build User Needs Fixing
Nathan Williams Approve
Review via email: mp+115826@code.launchpad.net

Description of the change

This branch heavily modfies the ClassIndex class and adds stronger unit tests. This was done in response to several critical bugs:

https://bugs.launchpad.net/akiban-persistit/+bug/1024857
https://bugs.launchpad.net/akiban-persistit/+bug/1026207
and a deadlock detected while testing.

At issue is that ClassIndex is very old and was never properly tested when used within the scope of a transaction, particularly a transaction that rolls back.

New implementation uses a private transaction scope within a private SessionId to perform all database activity. Before allowing a handle->class mapping to become visible in the cache, ClassIndex first commits the transaction that records it in the _classIndex tree. That transaction is not related to any enclosing transaction due to the use of a unique, private SessionId.

All update operations in the cache are synchronized. This is acceptable because so few updates occur in the lifetime of the system. All cache lookup operations are completed unsynchronized; they are safe by design for concurrent execution.

This branch adds new unit test to exercise multi-threaded operation.

To post a comment you must log in.
Revision history for this message
Peter Beaman (pbeaman) wrote :

Upon reflection I have decided to withdraw this proposal and replace the hash table with a ConcurrentHashMap. Back again soon.

review: Needs Fixing
Revision history for this message
Peter Beaman (pbeaman) wrote :

Upon further reflection I have decided to leave the existing hash table implementation. As written it explicitly supports the possibility of multiple versions per class, and does so without a map-of-maps.

Specifically, ClassIndex can track multiple versions of a class named X, differentiated only by their SerialVersionUID values. Different versions of a class get different handles.

In normal operation attempting to decode a Value recorded with a different version of the class than is now available in the classloader at least fails with a ConversionException rather than obscure failures deeper in the deserialization code. (Note that even with standard Java serialization, the ObjectStreamClass information has been removed from the Value and encoded in ClassIndex under the handle. Therefore the ClassIndex must supply the SerialVersionUID for verification.)

In deciding to leave the current implementation I added a unit test for handling multiple versions of the same class. This test uses the javax.tools.JavaCompiler class to create a class file, and therefore this test requires the JDK (specifically tools.jar) to run. This also adds a dependency on Java 6.

review: Needs Resubmitting
Revision history for this message
Nathan Williams (nwilliams) wrote :

Could/should Persistit.classIndex be final?

lookupByHandle:
I'm not so sure of our correctness here. AtomicReferenceArray provides atomic operations and volatile access semantics for the elements of the array but that doesn't extend down into the fields of the elements of the array. Specifically, I'm concerned about ClassInfoEntry._next. That field is updated in a synchronized block but accessed outside of one. A simple solution to that would probably be re-checking the list inside the synch block.

I'm also concerned with the growth of the hash table, but it isn't as severe. We save the hash table and search in it. Another thread could be doing this at the same time. Say that 2nd thread gets into the synch block first and updates the three and the map. We'll then find the stored ClassInfo correctly and go to update the table. We bump the size before we check that it wasn't added.

test2a() seems odd or maybe I don't quite get it. We're storing parts of the ClassIndex externally in a map and operating concurrently on it. Why doesn't this get ConcurrentModifcationExceptions regularly? Don't we also want to validate the true map (hash table) in the ClassIndex?

The multiThreaded and the manyClassInfo test seem more appropriate for stress tests, especially the latter given the compilation and additional requirements (JDK6/tools.jar).

review: Needs Fixing
Revision history for this message
Peter Beaman (pbeaman) wrote :

Excellent points.

ClassIndex is now final. So is the singleton reference to it in Persistit, and while there I made a bunch of other singletons final also.

ClassInfoEntry's constructor now sets the _next field and is immutable. I believe the memory model guarantee is that the object reference can't be visible until the constructor has finished.

HashTable growth - size now measure entries correctly.

The two multi-xxx tests are pretty quick and not inconsistent with other Persistit "unit" tests so I decided to leave them. But I did get rid of the use of tools.jar and the Compiler by using a much simpler technique.

review: Needs Resubmitting
Revision history for this message
Nathan Williams (nwilliams) wrote :

Thanks for the fixes, glad I wasn't off on an incorrect tangent. The makeAClass() hack is a little crazy, but probably better than the requirements of the other one.

So all good by me!

review: Approve
Revision history for this message
Akiban Build User (build-akiban) wrote :

There were 2 failures during build/test:

* job persistit-build failed at build number 384: http://172.16.20.104:8080/job/persistit-build/384/

* view must-pass failed: persistit-build is red

review: Needs Fixing
340. By Peter Beaman

Add license header

Revision history for this message
Peter Beaman (pbeaman) wrote :

Approving again after adding missing license header.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/main/java/com/persistit/ClassIndex.java'
2--- src/main/java/com/persistit/ClassIndex.java 2012-05-25 18:50:59 +0000
3+++ src/main/java/com/persistit/ClassIndex.java 2012-07-20 22:51:17 +0000
4@@ -46,39 +46,49 @@
5 * Note that certain handles for common classes are pre-assigned, and therefore
6 * are not translated through this class. See {@link Value} for details.
7 * </p>
8+ * <p>
9+ * Implementation note: this class implements a specialized hash table in which
10+ * entries are never removed. Almost always this class returns a value by
11+ * finding it in the hash table; the lookup is carefully designed to be
12+ * threadsafe without requiring synchronization. Only in the event of a cache
13+ * miss is there execution of a synchronized block which may read and/or update
14+ * the _classIndex tree.
15+ * </p>
16 *
17 * @version 1.1
18 */
19-class ClassIndex {
20+final class ClassIndex {
21 private final static int INITIAL_CAPACITY = 123;
22
23 private final static String BY_HANDLE = "byHandle";
24 private final static String BY_NAME = "byName";
25 private final static String NEXT_ID = "nextId";
26+ private final static int EXTRA_FACTOR = 2;
27+
28 final static String CLASS_INDEX_TREE_NAME = "_classIndex";
29
30- private AtomicInteger _size = new AtomicInteger();
31- private Persistit _persistit;
32+ private final AtomicInteger _size = new AtomicInteger();
33+ private final Persistit _persistit;
34+ private final SessionId _sessionId = new SessionId();
35
36- private AtomicReferenceArray<ClassInfoEntry> _hashById = new AtomicReferenceArray<ClassInfoEntry>(INITIAL_CAPACITY);
37- private AtomicReferenceArray<ClassInfoEntry> _hashByName = new AtomicReferenceArray<ClassInfoEntry>(
38+ private volatile AtomicReferenceArray<ClassInfoEntry> _hashTable = new AtomicReferenceArray<ClassInfoEntry>(
39 INITIAL_CAPACITY);
40- private ClassInfoEntry _knownNull = null;
41
42 private int _testIdFloor = Integer.MIN_VALUE;
43-
44+ private AtomicInteger _cacheMisses = new AtomicInteger();
45+ private AtomicInteger _discardedDuplicates = new AtomicInteger();
46+
47 /**
48 * A structure holding a ClassInfo, plus links to other related
49 * <code>ClassInfoEntry</code>s.
50 */
51 private static class ClassInfoEntry {
52- ClassInfoEntry _nextByIdHash;
53- ClassInfoEntry _nextByNameHash;
54-
55- ClassInfo _classInfo;
56-
57- ClassInfoEntry(ClassInfo ci) {
58+ final ClassInfoEntry _next;
59+ final ClassInfo _classInfo;
60+
61+ ClassInfoEntry(ClassInfo ci, ClassInfoEntry next) {
62 _classInfo = ci;
63+ _next = next;
64 }
65 }
66
67@@ -102,33 +112,32 @@
68 }
69
70 /**
71- * Looks up and returns the ClassInfo for an integer handle. This is used
72- * when decoding an <code>Object</code> from a
73- * <code>com.persistit.Value</code> to associate the encoded integer handle
74- * value with the corresponding class.
75+ * Look up and return the ClassInfo for an integer handle. This is used when
76+ * decoding an <code>Object</code> from a <code>com.persistit.Value</code>
77+ * to associate the encoded integer handle value with the corresponding
78+ * class.
79 *
80 * @param handle
81 * The handle
82 * @return The associated ClassInfo, or <i>null</i> if there is none.
83 */
84 public ClassInfo lookupByHandle(int handle) {
85- ClassInfoEntry cie = _hashById.get(handle % _hashById.length());
86+ AtomicReferenceArray<ClassInfoEntry> hashTable = _hashTable;
87+ ClassInfoEntry cie = hashTable.get(handle % hashTable.length());
88 while (cie != null) {
89 if (cie._classInfo.getHandle() == handle)
90 return cie._classInfo;
91- cie = cie._nextByIdHash;
92- }
93- cie = _knownNull;
94- while (cie != null) {
95- if (cie._classInfo.getHandle() == handle)
96- return null;
97- cie = cie._nextByIdHash;
98- }
99+ cie = cie._next;
100+ }
101+
102+ _cacheMisses.incrementAndGet();
103+
104 synchronized (this) {
105+ _sessionId.assign();
106 Exchange ex = null;
107 try {
108 ex = getExchange();
109- Transaction txn = ex.getTransaction();
110+ final Transaction txn = ex.getTransaction();
111 txn.begin();
112 try {
113 ex.clear().append(BY_HANDLE).append(handle).fetch();
114@@ -152,7 +161,7 @@
115 Class<?> cl = Class.forName(storedName, false, Thread.currentThread().getContextClassLoader());
116
117 long suid = 0;
118- ObjectStreamClass osc = ObjectStreamClass.lookup(cl);
119+ ObjectStreamClass osc = ObjectStreamClass.lookupAny(cl);
120 if (osc != null)
121 suid = osc.getSerialVersionUID();
122 if (storedSuid != suid) {
123@@ -164,9 +173,8 @@
124 return ci;
125 } else {
126 ClassInfo ci = new ClassInfo(null, 0, handle, null);
127- cie = _knownNull;
128- _knownNull = new ClassInfoEntry(ci);
129- _knownNull._nextByIdHash = cie;
130+ hashClassInfo(ci);
131+ return ci;
132 }
133 } catch (ClassNotFoundException cnfe) {
134 throw new ConversionException(cnfe);
135@@ -176,13 +184,11 @@
136 if (ex != null)
137 releaseExchange(ex);
138 }
139- // TODO - lookup in ClassIndex Exchange
140- return null;
141 }
142 }
143
144 /**
145- * Looks up and returns a ClassInfo for a class. This is used when encoding
146+ * Look up and return the ClassInfo for a class. This is used when encoding
147 * an <code>Object</code> into a <code>com.persistit.Value</code>.
148 *
149 * @param clazz
150@@ -190,20 +196,21 @@
151 * @return The ClassInfo for the specified Class.
152 */
153 public ClassInfo lookupByClass(Class<?> clazz) {
154+ AtomicReferenceArray<ClassInfoEntry> hashTable = _hashTable;
155
156 ObjectStreamClass osc = null;
157 long suid = 0;
158
159 int nh = clazz.getName().hashCode() & 0x7FFFFFFF;
160- ClassInfoEntry cie = _hashByName.get(nh % _hashById.length());
161+ ClassInfoEntry cie = hashTable.get(nh % hashTable.length());
162
163 while (cie != null) {
164- if (cie._classInfo.getDescribedClass().equals(clazz)) {
165+ if (clazz.equals(cie._classInfo.getDescribedClass())) {
166 return cie._classInfo;
167 }
168- if (cie._classInfo.getName().equals(clazz.getName())) {
169+ if (cie._classInfo.getDescribedClass() != null && cie._classInfo.getName().equals(clazz.getName())) {
170 if (osc == null) {
171- osc = ObjectStreamClass.lookup(clazz);
172+ osc = ObjectStreamClass.lookupAny(clazz);
173 if (osc != null) {
174 suid = osc.getSerialVersionUID();
175 }
176@@ -212,22 +219,49 @@
177 return cie._classInfo;
178 }
179 }
180- cie = cie._nextByNameHash;
181- }
182+ cie = cie._next;
183+ }
184+
185+ if (osc == null) {
186+ osc = ObjectStreamClass.lookupAny(clazz);
187+ }
188+ if (osc != null) {
189+ suid = osc.getSerialVersionUID();
190+ }
191+
192+ _cacheMisses.incrementAndGet();
193+
194+ /**
195+ * To update the tree, this class uses a unique SessionId and results in
196+ * using a unique Transaction context unrelated to the application
197+ * context. Therefore if an application does this:
198+ *
199+ * <pre>
200+ * <code>
201+ * txn.begin();
202+ * value.put(new SomeClass());
203+ * txn.rollback();
204+ * txn.end();
205+ * </code>
206+ * </pre>
207+ *
208+ * the class SomeClass will be registered even though the enclosing
209+ * transaction rolled back. This is important because other concurrent
210+ * threads may have started using the handle for SomeClass. Therefore
211+ * this class ensures that a non-nested transaction to insert the new
212+ * ClassInfo into the system volume has committed before adding the
213+ * handle to the hash table. </p>
214+ */
215
216 synchronized (this) {
217- if (osc == null) {
218- osc = ObjectStreamClass.lookup(clazz);
219- }
220- if (osc != null) {
221- suid = osc.getSerialVersionUID();
222- }
223+ final SessionId saveSessionId = _persistit.getSessionId();
224 Exchange ex = null;
225 try {
226+ _persistit.setSessionId(_sessionId);
227 ex = getExchange();
228+ final Transaction txn = ex.getTransaction();
229 final ClassInfo ci;
230 final int handle;
231- Transaction txn = ex.getTransaction();
232 txn.begin();
233 ex.clear().append(BY_NAME).append(clazz.getName()).append(suid).fetch();
234 Value value = ex.getValue();
235@@ -274,8 +308,10 @@
236 } catch (PersistitException pe) {
237 throw new ConversionException(pe);
238 } finally {
239- if (ex != null)
240+ if (ex != null) {
241 releaseExchange(ex);
242+ }
243+ _persistit.setSessionId(saveSessionId);
244 }
245 }
246 }
247@@ -295,45 +331,50 @@
248 }
249
250 private void hashClassInfo(ClassInfo ci) {
251- int size = _size.incrementAndGet();
252- if (size * 2 > _hashById.length()) {
253- AtomicReferenceArray<ClassInfoEntry> hashById = new AtomicReferenceArray<ClassInfoEntry>(size * 2);
254- AtomicReferenceArray<ClassInfoEntry> hashByName = new AtomicReferenceArray<ClassInfoEntry>(size * 2);
255- for (int i = 0; i < _hashById.length(); i++) {
256- ClassInfoEntry cie = _hashById.get(i);
257+ int size = _size.get();
258+ if (size * EXTRA_FACTOR > _hashTable.length()) {
259+ int discarded = _discardedDuplicates.get();
260+ AtomicReferenceArray<ClassInfoEntry> newHashTable = new AtomicReferenceArray<ClassInfoEntry>(EXTRA_FACTOR
261+ * 2 * size);
262+ for (int i = 0; i < _hashTable.length(); i++) {
263+ ClassInfoEntry cie = _hashTable.get(i);
264 while (cie != null) {
265- addHashEntry(cie._classInfo, hashById, hashByName);
266- cie = cie._nextByIdHash;
267+ addHashEntry(newHashTable, cie._classInfo);
268+ cie = cie._next;
269 }
270 }
271- _hashById = hashById;
272- _hashByName = hashByName;
273- }
274- addHashEntry(ci, _hashById, _hashByName);
275- }
276-
277- private void addHashEntry(final ClassInfo ci, final AtomicReferenceArray<ClassInfoEntry> hashById,
278- final AtomicReferenceArray<ClassInfoEntry> hashByName) {
279- ClassInfoEntry cie = new ClassInfoEntry(ci);
280- int handle = ci.getHandle();
281- int nh = ci.getName().hashCode() & 0x7FFFFFFF;
282-
283- ClassInfoEntry cie1 = hashById.get(handle % hashById.length());
284- ClassInfoEntry cie2 = hashByName.get(nh % hashByName.length());
285-
286- cie._nextByIdHash = cie1;
287- cie._nextByNameHash = cie2;
288-
289- hashById.set(handle % hashById.length(), cie);
290- hashByName.set(nh % hashByName.length(), cie);
291-
292- ClassInfoEntry cie3 = _knownNull;
293- while (cie3 != null) {
294- if (cie3._classInfo.getHandle() != handle) {
295- _knownNull = cie3._nextByIdHash;
296+ _hashTable = newHashTable;
297+ _discardedDuplicates.set(discarded);
298+ }
299+ addHashEntry(_hashTable, ci);
300+ }
301+
302+ private void addHashEntry(final AtomicReferenceArray<ClassInfoEntry> hashTable, ClassInfo ci) {
303+ final int hh = ci.getHandle() % hashTable.length();
304+ final int nh = ci.getDescribedClass() == null ? -1
305+ : ((ci.getDescribedClass().getName().hashCode() & 0x7FFFFFFF) % hashTable.length());
306+ boolean added = addHashEntry(hashTable, ci, hh);
307+ if (nh != -1 && nh != hh) {
308+ added |= addHashEntry(hashTable, ci, nh);
309+ }
310+ if (!added) {
311+ _discardedDuplicates.incrementAndGet();
312+ }
313+ }
314+
315+ private boolean addHashEntry(final AtomicReferenceArray<ClassInfoEntry> hashTable, ClassInfo ci, final int hash) {
316+ ClassInfoEntry cie = hashTable.get(hash);
317+ while (cie != null) {
318+ if (ci.equals(cie._classInfo)) {
319+ return false;
320 }
321- cie3 = cie3._nextByIdHash;
322+ cie = cie._next;
323 }
324+ cie = hashTable.get(hash);
325+ ClassInfoEntry newCie = new ClassInfoEntry(ci, cie);
326+ hashTable.set(hash, newCie);
327+ _size.incrementAndGet();
328+ return true;
329 }
330
331 private Exchange getExchange() throws PersistitException {
332@@ -348,22 +389,45 @@
333 private void releaseExchange(Exchange ex) {
334 _persistit.releaseExchange(ex);
335 }
336-
337+
338 /**
339- * For unit tests only. Next class ID handle will be at least
340- * as large as this.
341+ * For unit tests only. Next class ID handle will be at least as large as
342+ * this.
343+ *
344 * @param id
345 */
346 void setTestIdFloor(final int id) {
347 _testIdFloor = id;
348 }
349-
350+
351 /**
352- * For unit tests only. Clears all entries.
353+ * For unit tests only. Clears all entries.
354+ *
355 * @throws PersistitException
356 */
357 void clearAllEntries() throws PersistitException {
358 getExchange().removeAll();
359- }
360-
361+ _cacheMisses.set(0);
362+ _discardedDuplicates.set(0);
363+ }
364+
365+ /**
366+ * For unit tests only.
367+ *
368+ * @return count (since beginning or last call to {@link #clearAllEntries()}
369+ * ) of cache misses.
370+ */
371+ int getCacheMisses() {
372+ return _cacheMisses.get();
373+ }
374+
375+ /**
376+ * For unit tests only.
377+ *
378+ * @return count (since beginning or last call to {@link #clearAllEntries()}
379+ * ) of discarded duplicates.
380+ */
381+ int getDiscardedDuplicates() {
382+ return _discardedDuplicates.get();
383+ }
384 }
385
386=== modified file 'src/main/java/com/persistit/ClassInfo.java'
387--- src/main/java/com/persistit/ClassInfo.java 2012-05-25 18:50:59 +0000
388+++ src/main/java/com/persistit/ClassInfo.java 2012-07-20 22:51:17 +0000
389@@ -44,7 +44,7 @@
390 public boolean equals(final Object object) {
391 if (object instanceof ClassInfo) {
392 ClassInfo ci = (ClassInfo) object;
393- if (_class.equals(ci._class) && _handle == ci._handle && _suid == ci._suid) {
394+ if (equals(_class, ci._class) && _handle == ci._handle && _suid == ci._suid) {
395 return true;
396 }
397 }
398@@ -91,5 +91,9 @@
399 public ObjectStreamClass getClassDescriptor() {
400 return _osc;
401 }
402+
403+ private boolean equals(Class<?> a, Class<?> b) {
404+ return a == null ? b == null : a.equals(b);
405+ }
406 }
407
408
409=== modified file 'src/main/java/com/persistit/Persistit.java'
410--- src/main/java/com/persistit/Persistit.java 2012-06-30 15:12:11 +0000
411+++ src/main/java/com/persistit/Persistit.java 2012-07-20 22:51:17 +0000
412@@ -397,23 +397,22 @@
413 private final HashMap<Integer, BufferPool> _bufferPoolTable = new HashMap<Integer, BufferPool>();
414 private final ArrayList<Volume> _volumes = new ArrayList<Volume>();
415
416- private AtomicBoolean _initialized = new AtomicBoolean();
417- private AtomicBoolean _closed = new AtomicBoolean();
418- private AtomicBoolean _fatal = new AtomicBoolean();
419+ private final AtomicBoolean _initialized = new AtomicBoolean();
420+ private final AtomicBoolean _closed = new AtomicBoolean();
421+ private final AtomicBoolean _fatal = new AtomicBoolean();
422
423 private long _beginCloseTime;
424 private long _nextCloseTime;
425
426 private final LogBase _logBase = new LogBase();
427
428- private AtomicBoolean _suspendShutdown = new AtomicBoolean(false);
429- private AtomicBoolean _suspendUpdates = new AtomicBoolean(false);
430+ private final AtomicBoolean _suspendShutdown = new AtomicBoolean(false);
431+ private final AtomicBoolean _suspendUpdates = new AtomicBoolean(false);
432
433 private UtilControl _localGUI;
434
435- private AtomicReference<CoderManager> _coderManager = new AtomicReference<CoderManager>();
436-
437- private ClassIndex _classIndex = new ClassIndex(this);
438+ private final AtomicReference<CoderManager> _coderManager = new AtomicReference<CoderManager>();
439+ private final ClassIndex _classIndex = new ClassIndex(this);
440
441 private final ThreadLocal<SessionId> _sessionIdThreadLocal = new ThreadLocal<SessionId>() {
442 @Override
443
444=== modified file 'src/test/java/com/persistit/ClassIndexTest.java'
445--- src/test/java/com/persistit/ClassIndexTest.java 2012-05-25 18:50:59 +0000
446+++ src/test/java/com/persistit/ClassIndexTest.java 2012-07-20 22:51:17 +0000
447@@ -21,22 +21,50 @@
448 package com.persistit;
449
450 import static org.junit.Assert.assertEquals;
451+import static org.junit.Assert.assertNotNull;
452 import static org.junit.Assert.assertNull;
453 import static org.junit.Assert.assertTrue;
454
455+import java.io.InputStream;
456+import java.io.Serializable;
457 import java.lang.reflect.Field;
458-import java.util.HashMap;
459+import java.util.HashSet;
460 import java.util.Map;
461+import java.util.Set;
462+import java.util.concurrent.ConcurrentHashMap;
463
464 import org.junit.Test;
465
466+import com.persistit.Transaction.CommitPolicy;
467 import com.persistit.unit.PersistitUnitTestCase;
468+import com.persistit.util.Util;
469
470 public class ClassIndexTest extends PersistitUnitTestCase {
471
472+ /**
473+ * Arbitrary constant chosen to be found exactly once in the MockSerailizableObject
474+ * class file.
475+ */
476+ final static long SUID_CONSTANT = 0xF3E1D4C1B5A99286L;
477+ private final static String MOCK_SERIALIZABLE_CLASS_NAME = "MockSerializableObject";
478+
479 private int _maxHandle = 0;
480
481- final Map<Integer, ClassInfo> map = new HashMap<Integer, ClassInfo>();
482+ final Map<Integer, ClassInfo> map = new ConcurrentHashMap<Integer, ClassInfo>();
483+
484+ @SuppressWarnings("serial")
485+ public static class A implements Serializable {
486+
487+ }
488+
489+ @SuppressWarnings("serial")
490+ public static class B implements Serializable {
491+
492+ }
493+
494+ public static class C {
495+
496+ }
497
498 @Override
499 public void tearDown() throws Exception {
500@@ -45,7 +73,7 @@
501 }
502
503 @Test
504- public void testOneClassInfo() throws Exception {
505+ public void oneClassInfo() throws Exception {
506 final ClassIndex cx = _persistit.getClassIndex();
507 cx.registerClass(this.getClass());
508 ClassInfo ci = cx.lookupByClass(this.getClass());
509@@ -54,18 +82,22 @@
510 }
511
512 @Test
513- public void test2() throws Exception {
514+ public void manyClassInfo() throws Exception {
515 _maxHandle = 0;
516 final ClassIndex cx = _persistit.getClassIndex();
517 Class<?> clazz = Persistit.class;
518 test2a(cx, clazz);
519+
520+ assertEquals("Cache misses should match map size", map.size(), cx.getCacheMisses()
521+ - cx.getDiscardedDuplicates());
522+
523 for (int handle = 0; handle < _maxHandle + 10; handle++) {
524- assertEquals(map.get(handle), cx.lookupByHandle(handle));
525+ assertTrue(equals(map.get(handle), cx.lookupByHandle(handle)));
526 }
527
528 final ClassIndex cy = new ClassIndex(_persistit);
529 for (int handle = 0; handle < _maxHandle + 10; handle++) {
530- assertEquals(map.get(handle), cy.lookupByHandle(handle));
531+ assertTrue(equals(map.get(handle), cy.lookupByHandle(handle)));
532 }
533 for (int handle = 0; handle < _maxHandle + 10; handle++) {
534 ClassInfo ci = map.get(handle);
535@@ -84,6 +116,143 @@
536 System.out.println(cx.size() + " classes");
537 }
538
539+ @Test
540+ public void multiThreaded() throws Exception {
541+ final int threadCount = 50;
542+
543+ final Thread[] threads = new Thread[threadCount];
544+ for (int i = 0; i < threadCount; i++) {
545+ final int index = i;
546+ threads[i] = new Thread(new Runnable() {
547+ public void run() {
548+ final Transaction transaction = _persistit.getTransaction();
549+ for (int k = 0; k < 10; k++) {
550+ try {
551+ transaction.begin();
552+ try {
553+ test2a(_persistit.getClassIndex(), Persistit.class);
554+ if ((index % 3) == 0) {
555+ transaction.rollback();
556+ } else {
557+ transaction.commit();
558+ }
559+ } finally {
560+ transaction.end();
561+ }
562+ } catch (Exception e) {
563+ e.printStackTrace();
564+ }
565+ }
566+ }
567+ });
568+ }
569+ for (int i = 0; i < threadCount; i++) {
570+ threads[i].start();
571+ }
572+ for (int i = 0; i < threadCount; i++) {
573+ threads[i].join();
574+ }
575+
576+ final ClassIndex cx = _persistit.getClassIndex();
577+
578+ /*
579+ * Verify that almost all lookups were satisfied from cache. There
580+ * should be one "cache miss" for every class in the map. There may be
581+ * more: these are caused by concurrent execution and are explicitly
582+ * permitted - they will happen only rarely and only when multiple
583+ * threads are contending to register a new class. Such instances are
584+ * safely discarded and the following adjusts the cache miss count by
585+ * the number so discarded.
586+ */
587+ assertEquals("Cache misses should match map size", map.size(), cx.getCacheMisses()
588+ - cx.getDiscardedDuplicates());
589+
590+ for (int handle = 0; handle < _maxHandle + 10; handle++) {
591+ assertTrue(equals(map.get(handle), cx.lookupByHandle(handle)));
592+ }
593+
594+ }
595+
596+ @Test
597+ public void rollback() throws Exception {
598+ Exchange ex = _persistit.getExchange("persistit", "ClassIndexTest", true);
599+ Transaction txn = ex.getTransaction();
600+ txn.begin();
601+ try {
602+
603+ ex.getValue().put(new A());
604+ ex.to("A").store();
605+ ex.getValue().put(new B());
606+ ex.to("B").store();
607+ txn.rollback();
608+ } finally {
609+ txn.end();
610+ }
611+
612+ txn.begin();
613+ try {
614+ ex.getValue().put(new B());
615+ ex.to("B").store();
616+ ex.getValue().put(new A());
617+ ex.to("A").store();
618+ txn.commit(CommitPolicy.HARD);
619+ } finally {
620+ txn.end();
621+ }
622+
623+ final Configuration config = _persistit.getConfiguration();
624+ _persistit.crash();
625+ _persistit = new Persistit();
626+ _persistit.initialize(config);
627+ ex = _persistit.getExchange("persistit", "ClassIndexTest", false);
628+ Object b = ex.to("B").fetch().getValue().get();
629+ Object a = ex.to("A").fetch().getValue().get();
630+ assertTrue("Incorrect class", a instanceof A);
631+ assertTrue("Incorrect class", b instanceof B);
632+ }
633+
634+ @Test
635+ public void knownNull() throws Exception {
636+ ClassIndex cx = _persistit.getClassIndex();
637+ final ClassInfo ci = cx.lookupByHandle(12345);
638+ assertEquals("Should return cached known null", ci, cx.lookupByHandle(12345));
639+ }
640+
641+ @Test
642+ public void multipleVersions() throws Exception {
643+ ClassIndex cx = _persistit.getClassIndex();
644+ cx.clearAllEntries();
645+ int count = 10;
646+ final Class<?>[] classes = new Class<?>[count];
647+ for (int i = 0; i < count; i++) {
648+ classes[i] = makeAClass(i);
649+ assertNotNull("this test requires tools.jar on the classpath", classes[i]);
650+ cx.lookupByClass(classes[i]);
651+ }
652+ assertEquals("Each version should be unique", count, cx.getCacheMisses());
653+ Set<Integer> handles = new HashSet<Integer>();
654+ for (int i = 0; i < count; i++) {
655+ ClassInfo ci = cx.lookupByClass(classes[i]);
656+ int handle = ci.getHandle();
657+ assertTrue("Handles must be unique", handles.add(handle));
658+ }
659+ assertEquals("Each version should be in cache", count, cx.getCacheMisses());
660+ for (final Integer handle : handles) {
661+ ClassInfo ci = cx.lookupByHandle(handle);
662+ assertEquals("Lookup by handle and class must match", ci, cx.lookupByClass(ci.getDescribedClass()));
663+ }
664+ }
665+
666+ private boolean equals(final ClassInfo a, final ClassInfo b) {
667+ if (a == b) {
668+ return true;
669+ }
670+ if (a == null) {
671+ return b.getDescribedClass() == null;
672+ }
673+ return a.equals(b);
674+ }
675+
676 private void test2a(final ClassIndex cx, final Class<?> clazz) throws Exception {
677 if (clazz.isPrimitive()) {
678 return;
679@@ -103,13 +272,44 @@
680 } else {
681 assert (copy.equals(ci));
682 }
683-
684- }
685-
686- @Override
687- public void runAllTests() throws Exception {
688- // TODO Auto-generated method stub
689-
690+ }
691+
692+ private static Class<?> makeAClass(final long suid) throws Exception {
693+ InputStream is = ClassIndexTest.class.getClassLoader().getResourceAsStream(
694+ "com/persistit/" + MOCK_SERIALIZABLE_CLASS_NAME + "_classBytes");
695+ final byte[] bytes = new byte[10000];
696+ final int length = is.read(bytes);
697+ replaceSuid(bytes, suid);
698+ ClassLoader cl = new ClassLoader(ClassIndexTest.class.getClassLoader()) {
699+ public Class<?> loadClass(final String name) {
700+ if (name.contains(MOCK_SERIALIZABLE_CLASS_NAME)) {
701+ return defineClass(name, bytes, 0, length);
702+ } else {
703+ try {
704+ return ClassIndexTest.class.getClassLoader().loadClass(name);
705+ } catch (Exception e) {
706+ throw new RuntimeException(e);
707+ }
708+ }
709+ }
710+ };
711+ return cl.loadClass("com.persistit." + MOCK_SERIALIZABLE_CLASS_NAME);
712+ }
713+
714+ private final static void replaceSuid(final byte[] bytes, final long suid) {
715+ boolean replaced = false;
716+ for (int i = 0; i < bytes.length; i++) {
717+ boolean found = true;
718+ for (int j = 0; found && j < 8; j++) {
719+ found &= ((bytes[i + j] & 0xFF) == ((SUID_CONSTANT >>> (56 - j * 8)) & 0xFF));
720+ }
721+ if (found) {
722+ assertTrue("Found multiple instances of SUID_CONSTANT", !replaced);
723+ Util.putLong(bytes, i, suid);
724+ replaced = true;
725+ }
726+ }
727+ assertTrue("Did not find SUID_CONSTANT", replaced);
728 }
729
730 }
731
732=== added file 'src/test/java/com/persistit/MockSerializableObject.java'
733--- src/test/java/com/persistit/MockSerializableObject.java 1970-01-01 00:00:00 +0000
734+++ src/test/java/com/persistit/MockSerializableObject.java 2012-07-20 22:51:17 +0000
735@@ -0,0 +1,29 @@
736+/**
737+ * Copyright © 2011-2012 Akiban Technologies, Inc. All rights reserved.
738+ *
739+ * This program is free software: you can redistribute it and/or modify
740+ * it under the terms of the GNU Affero General Public License as
741+ * published by the Free Software Foundation, version 3 (only) of the
742+ * License.
743+ *
744+ * This program is distributed in the hope that it will be useful,
745+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
746+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
747+ * GNU Affero General Public License for more details.
748+ *
749+ * You should have received a copy of the GNU Affero General Public License
750+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
751+ *
752+ * This program may also be available under different license terms. For more
753+ * information, see www.akiban.com or contact licensing@akiban.com.
754+ */
755+
756+package com.persistit;
757+
758+import java.io.Serializable;
759+
760+public class MockSerializableObject implements Serializable {
761+
762+ private static final long serialVersionUID = ClassIndexTest.SUID_CONSTANT;
763+
764+}
765
766=== added file 'src/test/resources/com/persistit/MockSerializableObject_classBytes'
767Binary files src/test/resources/com/persistit/MockSerializableObject_classBytes 1970-01-01 00:00:00 +0000 and src/test/resources/com/persistit/MockSerializableObject_classBytes 2012-07-20 22:51:17 +0000 differ

Subscribers

People subscribed via source and target branches