Merge lp:~pbeaman/akiban-persistit/fix_classIndex into lp:akiban-persistit
- fix_classIndex
- Merge into trunk
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 | ||||||||
Related bugs: |
|
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 |
Commit message
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:/
https:/
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.
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.
Nathan Williams (nwilliams) wrote : | # |
Could/should Persistit.
lookupByHandle:
I'm not so sure of our correctness here. AtomicReference
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 ConcurrentModif
The multiThreaded and the manyClassInfo test seem more appropriate for stress tests, especially the latter given the compilation and additional requirements (JDK6/tools.jar).
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.
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!
Akiban Build User (build-akiban) wrote : | # |
There were 2 failures during build/test:
* job persistit-build failed at build number 384: http://
* view must-pass failed: persistit-build is red
- 340. By Peter Beaman
-
Add license header
Peter Beaman (pbeaman) wrote : | # |
Approving again after adding missing license header.
Preview Diff
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' |
767 | Binary 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 |
Upon reflection I have decided to withdraw this proposal and replace the hash table with a ConcurrentHashMap. Back again soon.