/*
* Copyright 2006-2012 Amazon Technologies, Inc. or its affiliates.
* Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
* of Amazon Technologies, Inc. or its affiliates. All rights reserved.
*
* 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.amazon.carbonado.layout;
import java.io.InputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import org.apache.commons.logging.LogFactory;
import com.amazon.carbonado.Cursor;
import com.amazon.carbonado.FetchDeadlockException;
import com.amazon.carbonado.FetchException;
import com.amazon.carbonado.FetchInterruptedException;
import com.amazon.carbonado.FetchNoneException;
import com.amazon.carbonado.FetchTimeoutException;
import com.amazon.carbonado.IsolationLevel;
import com.amazon.carbonado.PersistDeadlockException;
import com.amazon.carbonado.PersistException;
import com.amazon.carbonado.PersistTimeoutException;
import com.amazon.carbonado.Repository;
import com.amazon.carbonado.RepositoryException;
import com.amazon.carbonado.Storable;
import com.amazon.carbonado.Storage;
import com.amazon.carbonado.Transaction;
import com.amazon.carbonado.UniqueConstraintException;
import com.amazon.carbonado.capability.ResyncCapability;
import com.amazon.carbonado.info.StorableInfo;
import com.amazon.carbonado.info.StorableIntrospector;
import com.amazon.carbonado.info.StorableProperty;
import com.amazon.carbonado.info.StorablePropertyAdapter;
import com.amazon.carbonado.info.StorablePropertyAnnotation;
import com.amazon.carbonado.util.SoftValuedCache;
/**
* Factory for obtaining references to storable layouts.
*
* @author Brian S O'Neill
*/
public class LayoutFactory implements LayoutCapability {
// The first entry is the primary hash multiplier. Subsequent ones are
// rehash multipliers.
private static final int[] HASH_MULTIPLIERS = {31, 63};
final Repository mRepository;
final Storage<StoredLayout> mLayoutStorage;
final Storage<StoredLayoutProperty> mPropertyStorage;
private SoftValuedCache<Class<? extends Storable>, Layout> mReconstructed;
// Added this to allow consumers of the the main Carbonado to deal which
// changes to how LayoutFactories are reconstructed from streams.
public static final int VERSION = 2;
/**
* @throws com.amazon.carbonado.SupportException if underlying repository
* does not support the storables for persisting storable layouts
*/
public LayoutFactory(Repository repo) throws RepositoryException {
mRepository = repo;
mLayoutStorage = repo.storageFor(StoredLayout.class);
mPropertyStorage = repo.storageFor(StoredLayoutProperty.class);
}
/**
* Returns the layout matching the current definition of the given type.
*
* @throws PersistException if type represents a new generation, but
* persisting this information failed
*/
public Layout layoutFor(Class<? extends Storable> type)
throws FetchException, PersistException
{
return layoutFor(type, null);
}
/**
* Returns the layout matching the current definition of the given type.
*
* @throws PersistException if type represents a new generation, but
* persisting this information failed
*/
public Layout layoutFor(Class<? extends Storable> type, LayoutOptions options)
throws FetchException, PersistException
{
return layoutFor(false, type, options);
}
/**
* Returns the layout matching the current definition of the given type.
*
* @param readOnly if true, don't attempt to persist new generation because
* underlying repository is read-only
* @throws PersistException if type represents a new generation, but
* persisting this information failed
*/
public Layout layoutFor(final boolean readOnly,
final Class<? extends Storable> type, final LayoutOptions options)
throws FetchException, PersistException
{
if (options != null) {
// Make side-effect consistently applied.
options.readOnly();
}
synchronized (this) {
if (mReconstructed != null) {
Layout layout = mReconstructed.get(type);
if (layout != null) {
return layout;
}
}
}
final StorableInfo<?> info = StorableIntrospector.examine(type);
// Launch layout request in a new separate thread to ensure that
// transaction is top-level.
class LayoutRequest extends Thread {
private boolean done;
private Layout layout;
private FetchException fetchEx;
private PersistException persistEx;
private RuntimeException runtimeEx;
public synchronized void run() {
try {
try {
layout = layoutFor(readOnly, info, options);
} catch (FetchException e) {
fetchEx = e;
} catch (PersistException e) {
persistEx = e;
} catch (RuntimeException e) {
// This is a catchall for any other exception which
// might happen so that it doesn't get absorbed by
// this thread.
runtimeEx = e;
}
} finally {
done = true;
notifyAll();
}
}
synchronized Layout getLayout()
throws FetchException, PersistException, InterruptedException
{
while (!done) {
wait();
}
if (fetchEx != null) {
// Wrap to get complete stack trace.
throw new FetchException(fetchEx);
}
if (persistEx != null) {
// Wrap to get complete stack trace.
throw new PersistException(persistEx);
}
if (runtimeEx != null) {
// Wrap to get complete stack trace.
throw new RuntimeException(runtimeEx);
}
return layout;
}
}
LayoutRequest request = new LayoutRequest();
request.setDaemon(true);
request.start();
try {
return request.getLayout();
} catch (InterruptedException e) {
throw new FetchInterruptedException();
}
}
private Layout layoutFor(boolean readOnly, StorableInfo<?> info, LayoutOptions options)
throws FetchException, PersistException
{
Layout layout;
ResyncCapability resyncCap = null;
// Try to insert metadata up to three times.
boolean top = true;
loadLayout: for (int retryCount = 3;;) {
try {
Transaction txn;
if (top) {
txn = mRepository.enterTopTransaction(IsolationLevel.READ_COMMITTED);
} else {
txn = mRepository.enterTransaction(IsolationLevel.READ_COMMITTED);
}
txn.setForUpdate(!readOnly);
try {
// If type represents a new generation, then a new layout needs to
// be inserted.
Layout newLayout = null;
for (int i=0; i<HASH_MULTIPLIERS.length; i++) {
// Generate an identifier which has a high likelyhood of being unique.
long layoutID = mixInHash(0L, info, options, HASH_MULTIPLIERS[i]);
// Initially use for comparison purposes.
newLayout = new Layout(this, info, options, layoutID);
StoredLayout storedLayout = mLayoutStorage.prepare();
storedLayout.setLayoutID(layoutID);
if (!storedLayout.tryLoad()) {
// Not found, so break out and insert.
break;
}
Layout knownLayout = new Layout(this, storedLayout);
if (knownLayout.equalLayouts(newLayout)) {
// Type does not represent a new generation. Return
// existing layout.
layout = knownLayout;
break loadLayout;
}
if (knownLayout.getAllProperties().size() == 0) {
// This is clearly wrong. All Storables must have
// at least one property. Assume that layout record
// is corrupt so rebuild it.
break;
}
// If this point is reached, then there was a hash collision in
// the generated layout ID. This should be extremely rare.
// Rehash and try again.
if (i >= HASH_MULTIPLIERS.length - 1) {
// No more rehashes to attempt. This should be extremely,
// extremely rare, unless there is a bug somewhere.
throw new FetchException
("Unable to generate unique layout identifier for " +
info.getName());
}
}
// If this point is reached, then type represents a new
// generation. Calculate next generation value and insert.
// Note: The following query might find a record that
// didn't exist just a moment ago. This will cause a new
// generation value to be calculated, which is incorrect.
// Inserting the layout causes a unique constraint
// exception, which prevents the mistake from persisting.
assert(newLayout != null);
int generation = nextGeneration(mRepository, info.getStorableType().getName());
newLayout.insert(readOnly, generation);
layout = newLayout;
if (generation == 0) {
LogFactory.getLog(getClass())
.debug("New schema layout inserted: " + layout);
}
txn.commit();
} finally {
txn.exit();
}
break;
} catch (UniqueConstraintException e) {
// This might be caused by a transient replication error. Retry
// a few times before throwing exception. Wait up to a second
// before each retry.
retryCount = e.backoff(e, retryCount, 1000);
resyncCap = mRepository.getCapability(ResyncCapability.class);
} catch (FetchException e) {
if (e instanceof FetchDeadlockException || e instanceof FetchTimeoutException) {
// Might be caused by coarse locks. Switch to nested
// transaction to share the locks.
if (top) {
top = false;
retryCount = e.backoff(e, retryCount, 100);
continue;
}
}
throw e;
} catch (PersistException e) {
if (e instanceof PersistDeadlockException || e instanceof PersistTimeoutException) {
// Might be caused by coarse locks. Switch to nested
// transaction to share the locks.
if (top) {
top = false;
retryCount = e.backoff(e, retryCount, 100);
continue;
}
}
throw e;
}
}
if (!readOnly && resyncCap != null) {
// Make sure that all layout records are sync'd.
try {
resyncCap.resync(StoredLayoutProperty.class, 1.0, null);
} catch (RepositoryException e) {
throw e.toPersistException();
}
}
return layout;
}
/**
* Returns the layout for a particular generation of the given type.
*
* @param generation desired generation
* @throws FetchNoneException if generation not found
*/
public Layout layoutFor(Class<? extends Storable> type, int generation)
throws FetchException, FetchNoneException
{
StoredLayout storedLayout =
mLayoutStorage.query("storableTypeName = ? & generation = ?")
.with(type.getName()).with(generation)
.loadOne();
return new Layout(this, storedLayout);
}
/**
* Read a layout as written by {@link Layout#writeTo}.
*
* @since 1.2.2
*/
public Layout readLayoutFrom(InputStream in) throws IOException, RepositoryException {
Transaction txn = mRepository.enterTransaction();
try {
txn.setForUpdate(true);
StoredLayout storedLayout = mLayoutStorage.prepare();
storedLayout.readFrom(in);
try {
storedLayout.insert();
} catch (UniqueConstraintException e) {
StoredLayout existing = mLayoutStorage.prepare();
storedLayout.copyPrimaryKeyProperties(existing);
if (existing.tryLoad()) {
// Only check subset of primary and alternate keys. The check
// of layout properties is more important.
if (!existing.getStorableTypeName().equals(storedLayout.getStorableTypeName()))
{
throw e;
}
storedLayout = existing;
} else {
// Assume alternate key constraint, so increment the generation.
storedLayout.setGeneration
(nextGeneration(mRepository, storedLayout.getStorableTypeName()));
storedLayout.insert();
}
}
int op;
while ((op = in.read()) != 0) {
StoredLayoutProperty storedProperty = mPropertyStorage.prepare();
storedProperty.readFrom(in);
try {
storedProperty.insert();
} catch (UniqueConstraintException e) {
StoredLayoutProperty existing = mPropertyStorage.prepare();
storedProperty.copyPrimaryKeyProperties(existing);
if (!existing.tryLoad()) {
throw e;
}
storedProperty.copyVersionProperty(existing);
if (!existing.equalProperties(storedProperty)) {
throw e;
}
}
}
txn.commit();
return new Layout(this, storedLayout);
} finally {
txn.exit();
}
}
synchronized void registerReconstructed
(Class<? extends Storable> reconstructed, Layout layout)
{
if (mReconstructed == null) {
mReconstructed = SoftValuedCache.newCache(7);
}
mReconstructed.put(reconstructed, layout);
}
static int nextGeneration(Repository repo, String typeName) throws FetchException {
int highestGen = -1;
{
Cursor<StoredLayout> cursor;
try {
cursor = repo.storageFor(StoredLayout.class).query("storableTypeName = ?")
.with(typeName).orderBy("-generation").fetchSlice(0, 1L);
} catch (RepositoryException e) {
throw e.toFetchException();
}
try {
if (cursor.hasNext()) {
highestGen = cursor.next().getGeneration();
}
} finally {
cursor.close();
}
}
Storage<StoredLayoutEquivalence> es;
try {
es = repo.storageFor(StoredLayoutEquivalence.class);
} catch (RepositoryException e) {
throw e.toFetchException();
}
Cursor<StoredLayoutEquivalence> cursor = es.query("storableTypeName = ?")
.with(typeName).orderBy("-generation").fetchSlice(0, 1L);
try {
if (cursor.hasNext()) {
highestGen = Math.max(highestGen, cursor.next().getGeneration());
}
} finally {
cursor.close();
}
cursor = es.query("storableTypeName = ?")
.with(typeName).orderBy("-matchedGeneration").fetchSlice(0, 1L);
try {
if (cursor.hasNext()) {
highestGen = Math.max(highestGen, cursor.next().getMatchedGeneration());
}
} finally {
cursor.close();
}
return highestGen + 1;
}
/**
* Creates a long hash code that attempts to mix in all relevant layout
* elements.
*/
private long mixInHash(long hash, StorableInfo<?> info, LayoutOptions options, int multiplier)
{
hash = mixInHash(hash, info.getStorableType().getName(), multiplier);
hash = mixInHash(hash, options, multiplier);
for (StorableProperty<?> property : info.getAllProperties().values()) {
if (!property.isJoin()) {
hash = mixInHash(hash, property, multiplier);
}
}
return hash;
}
/**
* Creates a long hash code that attempts to mix in all relevant layout
* elements.
*/
private long mixInHash(long hash, StorableProperty<?> property, int multiplier) {
hash = mixInHash(hash, property.getName(), multiplier);
hash = mixInHash(hash, property.getType().getName(), multiplier);
hash = hash * multiplier + (property.isNullable() ? 1 : 2);
hash = hash * multiplier + (property.isPrimaryKeyMember() ? 1 : 2);
// Keep this in for compatibility with prior versions of hash code.
hash = hash * multiplier + 1;
if (property.getAdapter() != null) {
// Keep this in for compatibility with prior versions of hash code.
hash += 1;
StorablePropertyAdapter adapter = property.getAdapter();
StorablePropertyAnnotation annotation = adapter.getAnnotation();
hash = mixInHash(hash, annotation.getAnnotationType().getName(), multiplier);
// Annotation may contain parameters which affect how property
// value is stored. So mix that in too.
Annotation ann = annotation.getAnnotation();
if (ann != null) {
hash = hash * multiplier + annHashCode(ann);
}
}
return hash;
}
private long mixInHash(long hash, CharSequence value, int multiplier) {
for (int i=value.length(); --i>=0; ) {
hash = hash * multiplier + value.charAt(i);
}
return hash;
}
private long mixInHash(long hash, LayoutOptions options, int multiplier) {
if (options != null) {
byte[] data = options.encode();
if (data != null) {
for (int b : data) {
hash = hash * multiplier + (b & 0xff);
}
}
}
return hash;
}
/**
* Returns an annotation hash code using a algorithm similar to the
* default. The difference is in the handling of class and enum values. The
* name is chosen for the hash code component instead of the instance
* because it is stable between invocations of the JVM.
*/
private static int annHashCode(Annotation ann) {
int hash = 0;
Method[] methods = ann.getClass().getDeclaredMethods();
for (Method m : methods) {
if (m.getReturnType() == null || m.getReturnType() == void.class) {
continue;
}
if (m.getParameterTypes().length != 0) {
continue;
}
String name = m.getName();
if (name.equals("hashCode") ||
name.equals("toString") ||
name.equals("annotationType"))
{
continue;
}
Object value;
try {
value = m.invoke(ann);
} catch (InvocationTargetException e) {
continue;
} catch (IllegalAccessException e) {
continue;
}
hash += (127 * name.hashCode()) ^ annValueHashCode(value);
}
return hash;
}
private static int annValueHashCode(Object value) {
Class type = value.getClass();
if (!type.isArray()) {
if (value instanceof String || type.isPrimitive()) {
return value.hashCode();
} else if (value instanceof Class) {
// Use name for stable hash code.
return ((Class) value).getName().hashCode();
} else if (value instanceof Enum) {
// Use name for stable hash code.
return ((Enum) value).name().hashCode();
} else if (value instanceof Annotation) {
return annHashCode((Annotation) value);
} else {
return value.hashCode();
}
} else if (type == byte[].class) {
return Arrays.hashCode((byte[]) value);
} else if (type == char[].class) {
return Arrays.hashCode((char[]) value);
} else if (type == double[].class) {
return Arrays.hashCode((double[]) value);
} else if (type == float[].class) {
return Arrays.hashCode((float[]) value);
} else if (type == int[].class) {
return Arrays.hashCode((int[]) value);
} else if (type == long[].class) {
return Arrays.hashCode((long[]) value);
} else if (type == short[].class) {
return Arrays.hashCode((short[]) value);
} else if (type == boolean[].class) {
return Arrays.hashCode((boolean[]) value);
} else {
return Arrays.hashCode((Object[]) value);
}
}
}