/**
* Copyright 2005-2012 Akiban Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.persistit;
import java.io.ObjectStreamClass;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceArray;
import com.persistit.exception.ConversionException;
import com.persistit.exception.PersistitException;
/**
* <p>
* A singleton that associates <code>Class</code>es with persistent handles used
* to refer to them in Persistit™ {@link Value}'s and {@link Key}s. When
* <code>Value</code> encodes an <code>Object</code>, rather than recording the
* object's full class name, it stores an integer-valued handle. The handle is
* associated by the <code>ClassIndex</code> to the class name. This mechanism
* minimizes the storage of redundant information in the potentially numerous
* stored instances of the same class.
* </p>
* <p>
* By default, the persistent storage for this association is located in a tree
* called <code>"_classIndex"</code> of the
* {@link com.persistit.Persistit#getSystemVolume system volume}.
* </p>
* <p>
* Note that certain handles for common classes are pre-assigned, and therefore
* are not translated through this class. See {@link Value} for details.
* </p>
* <p>
* Implementation note: this class implements a specialized hash table in which
* entries are never removed. Almost always this class returns a value by
* finding it in the hash table; the lookup is carefully designed to be
* threadsafe without requiring synchronization. Only in the event of a cache
* miss is there execution of a synchronized block which may read and/or update
* the _classIndex tree.
* </p>
*
* @version 1.1
*/
final class ClassIndex {
final static int HANDLE_BASE = 64;
private final static int INITIAL_CAPACITY = 123;
private final static String BY_HANDLE = "byHandle";
private final static String BY_NAME = "byName";
private final static String NEXT_ID = "nextId";
private final static int EXTRA_FACTOR = 2;
final static String CLASS_INDEX_TREE_NAME = "_classIndex";
private final AtomicInteger _size = new AtomicInteger();
private final Persistit _persistit;
private final SessionId _sessionId = new SessionId();
private volatile AtomicReferenceArray<ClassInfoEntry> _hashTable = new AtomicReferenceArray<ClassInfoEntry>(
INITIAL_CAPACITY);
private int _testIdFloor = Integer.MIN_VALUE;
private final AtomicInteger _cacheMisses = new AtomicInteger();
private final AtomicInteger _discardedDuplicates = new AtomicInteger();
/**
* A structure holding a ClassInfo, plus links to other related
* <code>ClassInfoEntry</code>s.
*/
private static class ClassInfoEntry {
final ClassInfoEntry _next;
final ClassInfo _classInfo;
ClassInfoEntry(final ClassInfo ci, final ClassInfoEntry next) {
_classInfo = ci;
_next = next;
}
}
/**
* Package-private constructor used only by {@link Persistit} during
* initialization.
*
* @param persistit
* Owning Persistit instance.
* @throws PersistitException
*/
ClassIndex(final Persistit persistit) {
_persistit = persistit;
}
void initialize() throws PersistitException {
/*
* Called during Persistit initialization. This has the desired
* side-effect of the class index tree outside of a transaction so that
* its existence is primordial.
*/
getExchange();
}
/**
* @return Number of <code>ClassInfo</code> objects currently stored in this
* ClassIndex.
*/
public int size() {
return _size.get();
}
/**
* Look up and return the ClassInfo for an integer handle. This is used when
* decoding an <code>Object</code> from a <code>com.persistit.Value</code>
* to associate the encoded integer handle value with the corresponding
* class.
*
* @param handle
* The handle
* @return The associated ClassInfo, or <i>null</i> if there is none.
*/
public ClassInfo lookupByHandle(final int handle) {
final AtomicReferenceArray<ClassInfoEntry> hashTable = _hashTable;
ClassInfoEntry cie = hashTable.get(handle % hashTable.length());
while (cie != null) {
if (cie._classInfo.getHandle() == handle)
return cie._classInfo;
cie = cie._next;
}
_cacheMisses.incrementAndGet();
synchronized (this) {
_sessionId.assign();
Exchange ex = null;
try {
ex = getExchange();
final Transaction txn = ex.getTransaction();
txn.begin();
try {
ex.clear().append(BY_HANDLE).append(handle).fetch();
txn.commit();
} catch (final Exception e) {
_persistit.getLogBase().exception.log(e);
throw new ConversionException(e);
} finally {
txn.end();
}
final Value value = ex.getValue();
if (value.isDefined()) {
value.setStreamMode(true);
final int storedId = value.getInt();
final String storedName = value.getString();
final long storedSuid = value.getLong();
if (storedId != handle) {
throw new IllegalStateException("ClassInfo stored for handle=" + handle
+ " has invalid stored handle=" + storedId);
}
final Class<?> cl = Class
.forName(storedName, false, Thread.currentThread().getContextClassLoader());
long suid = 0;
final ObjectStreamClass osc = ObjectStreamClass.lookupAny(cl);
if (osc != null)
suid = osc.getSerialVersionUID();
if (storedSuid != suid) {
throw new ConversionException("Class " + cl.getName() + " persistent SUID=" + storedSuid
+ " does not match current class SUID=" + suid);
}
final ClassInfo ci = new ClassInfo(cl, suid, handle, osc);
hashClassInfo(ci);
return ci;
} else {
final ClassInfo ci = new ClassInfo(null, 0, handle, null);
hashClassInfo(ci);
return ci;
}
} catch (final ClassNotFoundException cnfe) {
throw new ConversionException(cnfe);
} catch (final PersistitException pe) {
throw new ConversionException(pe);
} finally {
if (ex != null) {
releaseExchange(ex);
}
}
}
}
/**
* Look up and return the ClassInfo for a class. This is used when encoding
* an <code>Object</code> into a <code>com.persistit.Value</code>.
*
* @param clazz
* The <code>Class</code>
* @return The ClassInfo for the specified Class.
*/
public ClassInfo lookupByClass(final Class<?> clazz) {
final AtomicReferenceArray<ClassInfoEntry> hashTable = _hashTable;
ObjectStreamClass osc = null;
long suid = 0;
final int nh = clazz.getName().hashCode() & 0x7FFFFFFF;
ClassInfoEntry cie = hashTable.get(nh % hashTable.length());
while (cie != null) {
if (clazz.equals(cie._classInfo.getDescribedClass())) {
return cie._classInfo;
}
if (cie._classInfo.getDescribedClass() != null && cie._classInfo.getName().equals(clazz.getName())) {
if (osc == null) {
osc = ObjectStreamClass.lookupAny(clazz);
if (osc != null) {
suid = osc.getSerialVersionUID();
}
}
if (suid == cie._classInfo.getSUID()) {
return cie._classInfo;
}
}
cie = cie._next;
}
if (osc == null) {
osc = ObjectStreamClass.lookupAny(clazz);
}
if (osc != null) {
suid = osc.getSerialVersionUID();
}
_cacheMisses.incrementAndGet();
/**
* To update the tree, this class uses a unique SessionId and results in
* using a unique Transaction context unrelated to the application
* context. Therefore if an application does this:
*
* <pre>
* <code>
* txn.begin();
* value.put(new SomeClass());
* txn.rollback();
* txn.end();
* </code>
* </pre>
*
* the class SomeClass will be registered even though the enclosing
* transaction rolled back. This is important because other concurrent
* threads may have started using the handle for SomeClass. Therefore
* this class ensures that a non-nested transaction to insert the new
* ClassInfo into the system volume has committed before adding the
* handle to the hash table. </p>
*/
synchronized (this) {
final SessionId saveSessionId = _persistit.getSessionId();
Exchange ex = null;
try {
_persistit.setSessionId(_sessionId);
ex = getExchange();
final Transaction txn = ex.getTransaction();
final ClassInfo ci;
final int handle;
txn.begin();
ex.clear().append(BY_NAME).append(clazz.getName()).append(suid).fetch();
final Value value = ex.getValue();
try {
if (value.isDefined()) {
value.setStreamMode(true);
handle = value.getInt();
final String storedName = value.getString();
final long storedSuid = value.getLong();
if (storedSuid != suid || !clazz.getName().equals(storedName)) {
throw new ConversionException("Class " + clazz.getName() + " persistent SUID=" + storedSuid
+ " does not match current class SUID=" + suid);
}
ci = new ClassInfo(clazz, suid, handle, osc);
} else {
//
// Store a new ClassInfo record
//
ex.clear().append(NEXT_ID).fetch();
handle = Math.max(_testIdFloor, value.isDefined() ? value.getInt() : HANDLE_BASE) + 1;
value.clear().put(handle);
ex.store();
value.clear();
value.setStreamMode(true);
value.put(handle);
value.put(clazz.getName());
value.put(suid);
ex.clear().append(BY_NAME).append(clazz.getName()).append(suid).store();
ex.clear().append(BY_HANDLE).append(handle).store();
ci = new ClassInfo(clazz, suid, handle, osc);
}
txn.commit();
hashClassInfo(ci);
return ci;
} finally {
txn.end();
}
} catch (final PersistitException pe) {
throw new ConversionException(pe);
} finally {
if (ex != null) {
releaseExchange(ex);
}
_persistit.setSessionId(saveSessionId);
}
}
}
/**
* Registers a <code>Class</code>, which binds it permanently with a handle.
* The order in which classes are first registered governs the order in
* which <code>Key</code> values containing objects of the classes are
* sorted. See {@link com.persistit.encoding.CoderManager} for further
* information.
*
* @param clazz
* Class instance to register.
*/
public void registerClass(final Class<?> clazz) {
lookupByClass(clazz);
}
private void hashClassInfo(final ClassInfo ci) {
final int size = _size.get();
if (size * EXTRA_FACTOR > _hashTable.length()) {
final int discarded = _discardedDuplicates.get();
final AtomicReferenceArray<ClassInfoEntry> newHashTable = new AtomicReferenceArray<ClassInfoEntry>(
EXTRA_FACTOR * 2 * size);
for (int i = 0; i < _hashTable.length(); i++) {
ClassInfoEntry cie = _hashTable.get(i);
while (cie != null) {
addHashEntry(newHashTable, cie._classInfo);
cie = cie._next;
}
}
_hashTable = newHashTable;
_discardedDuplicates.set(discarded);
}
addHashEntry(_hashTable, ci);
}
private void addHashEntry(final AtomicReferenceArray<ClassInfoEntry> hashTable, final ClassInfo ci) {
final int hh = ci.getHandle() % hashTable.length();
final int nh = ci.getDescribedClass() == null ? -1
: ((ci.getDescribedClass().getName().hashCode() & 0x7FFFFFFF) % hashTable.length());
boolean added = addHashEntry(hashTable, ci, hh);
if (nh != -1 && nh != hh) {
added |= addHashEntry(hashTable, ci, nh);
}
if (!added) {
_discardedDuplicates.incrementAndGet();
}
}
private boolean addHashEntry(final AtomicReferenceArray<ClassInfoEntry> hashTable, final ClassInfo ci,
final int hash) {
ClassInfoEntry cie = hashTable.get(hash);
while (cie != null) {
if (ci.equals(cie._classInfo)) {
return false;
}
cie = cie._next;
}
cie = hashTable.get(hash);
final ClassInfoEntry newCie = new ClassInfoEntry(ci, cie);
hashTable.set(hash, newCie);
_size.incrementAndGet();
return true;
}
private Exchange getExchange() throws PersistitException {
try {
final Volume volume = _persistit.getSystemVolume();
return _persistit.getExchange(volume, CLASS_INDEX_TREE_NAME, true);
} catch (final PersistitException pe) {
throw new ConversionException(pe);
}
}
private void releaseExchange(final Exchange ex) {
_persistit.releaseExchange(ex);
}
/**
* For unit tests only. Next class ID handle will be at least as large as
* this.
*
* @param id
*/
void setTestIdFloor(final int id) {
_testIdFloor = id;
}
/**
* For unit tests only. Clears all entries.
*
* @throws PersistitException
*/
void clearAllEntries() throws PersistitException {
getExchange().removeAll();
_cacheMisses.set(0);
_discardedDuplicates.set(0);
}
/**
* For unit tests only.
*
* @return count (since beginning or last call to {@link #clearAllEntries()}
* ) of cache misses.
*/
int getCacheMisses() {
return _cacheMisses.get();
}
/**
* For unit tests only.
*
* @return count (since beginning or last call to {@link #clearAllEntries()}
* ) of discarded duplicates.
*/
int getDiscardedDuplicates() {
return _discardedDuplicates.get();
}
}