/* * Copyright 2007-2010 Brian S O'Neill * * 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. */ /* * Copyright 2006 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 org.cojen.util; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicReference; import java.util.Map; import java.lang.reflect.Method; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.security.AccessController; import java.security.PrivilegedAction; import org.cojen.classfile.CodeBuilder; import org.cojen.classfile.ClassFile; import org.cojen.classfile.Label; import org.cojen.classfile.MethodDesc; import org.cojen.classfile.Modifiers; import org.cojen.classfile.RuntimeClassFile; import org.cojen.classfile.TypeDesc; /** * Generic one-shot factory which supports late object creation. If the object * creation results in an exception or is taking too long, the object produced * instead is a bogus one. After retrying, if the real object is created, then * the bogus object turns into a wrapper to the real object. * * <p>Note: If a bogus object is created, the wrapper cannot always be a drop-in * replacement for the real object. If the wrapper is cloned, it won't have the * same behavior as cloning the real object. Also, synchronizing on the wrapper * will not synchronize the real object. * * @author Brian S O'Neill * @since 2.1 */ public abstract class BelatedCreator<T, E extends Exception> { private static final String REF_FIELD_NAME = "ref"; private static final Map<Class<?>, Class<?>> cWrapperCache; private static final ExecutorService cThreadPool; static { cWrapperCache = new SoftValuedHashMap(); cThreadPool = Executors.newCachedThreadPool(new TFactory()); } private final Class<T> mType; final int mMinRetryDelayMillis; private T mReal; private boolean mFailed; private Throwable mFailedError; private T mBogus; private AtomicReference<T> mRef; private CreateThread mCreateThread; /** * @param type type of object created * @param minRetryDelayMillis minimum milliseconds to wait before retrying * to create object after failure; if negative, never retry * @throws IllegalArgumentException if type is null or is not an interface */ protected BelatedCreator(Class<T> type, int minRetryDelayMillis) { if (type == null) { throw new IllegalArgumentException("Type is null"); } if (!type.isInterface()) { throw new IllegalArgumentException("Type must be an interface: " + type); } mType = type; mMinRetryDelayMillis = minRetryDelayMillis; } /** * Returns real or bogus object. If real object is returned, then future * invocations of this method return the same real object instance. This * method waits for the real object to be created, if it is blocked. If * real object creation fails immediately, then this method will not wait, * returning a bogus object immediately instead. * * @param timeoutMillis maximum time to wait for real object before * returning bogus one; if negative, potentially wait forever * @throws E exception thrown from createReal */ public synchronized T get(final int timeoutMillis) throws E { if (mReal != null) { return mReal; } if (mBogus != null && mMinRetryDelayMillis < 0) { return mBogus; } if (mCreateThread == null) { mCreateThread = new CreateThread(); cThreadPool.submit(mCreateThread); } if (timeoutMillis != 0) { final long start = System.nanoTime(); try { if (timeoutMillis < 0) { while (mReal == null && mCreateThread != null) { if (timeoutMillis < 0) { wait(); } } } else { long remaining = timeoutMillis; while (mReal == null && mCreateThread != null && !mFailed) { wait(remaining); long elapsed = (System.nanoTime() - start) / 1000000L; if ((remaining -= elapsed) <= 0) { break; } } } } catch (InterruptedException e) { } if (mReal != null) { return mReal; } long elapsed = (System.nanoTime() - start) / 1000000L; if (elapsed >= timeoutMillis) { timedOutNotification(elapsed); } } if (mFailedError != null) { // Take ownership of error. Also stitch traces together for // context. This gives the illusion that the creation attempt // occurred in this thread. Throwable error = mFailedError; mFailedError = null; StackTraceElement[] trace = error.getStackTrace(); error.fillInStackTrace(); StackTraceElement[] localTrace = error.getStackTrace(); StackTraceElement[] completeTrace = new StackTraceElement[trace.length + localTrace.length]; System.arraycopy(trace, 0, completeTrace, 0, trace.length); System.arraycopy(localTrace, 0, completeTrace, trace.length, localTrace.length); error.setStackTrace(completeTrace); ThrowUnchecked.fire(error); } if (mBogus == null) { mRef = new AtomicReference<T>(createBogus()); mBogus = AccessController.doPrivileged(new PrivilegedAction<T>() { public T run() { try { return getWrapper().newInstance(mRef); } catch (Exception e) { ThrowUnchecked.fire(e); return null; } } }); } return mBogus; } /** * Create instance of real object. If there is a recoverable error creating * the object, return null. Any error logging must be performed by the * implementation of this method. If null is returned, expect this method * to be called again in the future. * * @return real object, or null if there was a recoverable error * @throws E unrecoverable error */ protected abstract T createReal() throws E; /** * Create instance of bogus object. */ protected abstract T createBogus(); /** * Notification that createReal is taking too long. This can be used to log * a message. * * @param timedOutMillis milliseconds waited before giving up */ protected abstract void timedOutNotification(long timedOutMillis); /** * Notification that createReal has produced the real object. The default * implementation does nothing. */ protected void createdNotification(T object) { } synchronized void created(T object) { mReal = object; if (mBogus != null) { mBogus = null; if (mRef != null) { // Changing reference to real object. The ref object is also // held by the auto-generated wrapper, so changing here changes // the wrapper's behavior. mRef.set(object); } } mFailed = false; notifyAll(); createdNotification(object); } synchronized void failed() { if (mReal == null) { mFailed = true; } notifyAll(); } /** * @param error optional error to indicate thread is exiting because of this */ synchronized void handleThreadExit(Throwable error) { if (mReal == null) { mFailed = true; if (error != null) { mFailedError = error; } } mCreateThread = null; notifyAll(); } /** * Returns a Constructor that accepts an AtomicReference to the wrapped * object. */ private Constructor<T> getWrapper() { Class<T> clazz; synchronized (cWrapperCache) { clazz = (Class<T>) cWrapperCache.get(mType); if (clazz == null) { clazz = createWrapper(); cWrapperCache.put(mType, clazz); } } try { return clazz.getConstructor(AtomicReference.class); } catch (NoSuchMethodException e) { ThrowUnchecked.fire(e); return null; } } private Class<T> createWrapper() { RuntimeClassFile cf = new RuntimeClassFile(mType.getName()); cf.addInterface(mType); cf.markSynthetic(); cf.setSourceFile(BelatedCreator.class.getName()); cf.setTarget("1.5"); final TypeDesc atomicRefType = TypeDesc.forClass(AtomicReference.class); cf.addField(Modifiers.PRIVATE.toFinal(true), REF_FIELD_NAME, atomicRefType); CodeBuilder b = new CodeBuilder(cf.addConstructor(Modifiers.PUBLIC, new TypeDesc[] {atomicRefType})); b.loadThis(); b.invokeSuperConstructor(null); b.loadThis(); b.loadLocal(b.getParameter(0)); b.storeField(REF_FIELD_NAME, atomicRefType); b.returnVoid(); // Now define all interface methods to call wrapped object. for (Method m : mType.getMethods()) { try { Object.class.getMethod(m.getName(), m.getParameterTypes()); // Defined in object too, so skip it for now continue; } catch (NoSuchMethodException e) { } addWrappedCall(cf, new CodeBuilder(cf.addMethod(m)), m); } // Also wrap non-final public methods from Object. mType is an // interface, so we don't have to worry about any superclasses -- // except Object. We want to make sure that all (non-final) Object // methods delegate to the generated proxy. For example, one would // expect toString to call the wrapped object, not the wrapper itself. for (Method m : Object.class.getMethods()) { int modifiers = m.getModifiers(); if (!Modifier.isFinal(modifiers) && Modifier.isPublic(modifiers)) { b = new CodeBuilder (cf.addMethod(Modifiers.PUBLIC, m.getName(), MethodDesc.forMethod(m))); addWrappedCall(cf, b, m); } } Class<T> clazz = cf.defineClass(); return clazz; } private void addWrappedCall(ClassFile cf, CodeBuilder b, Method m) { // Special behavior for equals method boolean isEqualsMethod = false; if (m.getName().equals("equals") && m.getReturnType().equals(boolean.class)) { Class[] paramTypes = m.getParameterTypes(); isEqualsMethod = paramTypes.length == 1 && paramTypes[0].equals(Object.class); } if (isEqualsMethod) { b.loadThis(); b.loadLocal(b.getParameter(0)); Label notEqual = b.createLabel(); b.ifEqualBranch(notEqual, false); b.loadConstant(true); b.returnValue(TypeDesc.BOOLEAN); notEqual.setLocation(); // Check if object is our type. b.loadLocal(b.getParameter(0)); b.instanceOf(cf.getType()); Label isInstance = b.createLabel(); b.ifZeroComparisonBranch(isInstance, "!="); b.loadConstant(false); b.returnValue(TypeDesc.BOOLEAN); isInstance.setLocation(); } final TypeDesc atomicRefType = TypeDesc.forClass(AtomicReference.class); // Load wrapped object... b.loadThis(); b.loadField(REF_FIELD_NAME, atomicRefType); b.invokeVirtual(atomicRefType, "get", TypeDesc.OBJECT, null); b.checkCast(TypeDesc.forClass(mType)); // Load parameters... for (int i=0; i<b.getParameterCount(); i++) { b.loadLocal(b.getParameter(i)); } // Special behavior for equals method if (isEqualsMethod) { // Extract wrapped object. b.checkCast(cf.getType()); b.loadField(REF_FIELD_NAME, atomicRefType); b.invokeVirtual(atomicRefType, "get", TypeDesc.OBJECT, null); b.checkCast(TypeDesc.forClass(mType)); } // Invoke wrapped method... b.invoke(m); if (m.getReturnType() == void.class) { b.returnVoid(); } else { b.returnValue(TypeDesc.forClass(m.getReturnType())); } } private class CreateThread implements Runnable { public void run() { try { while (true) { T real = createReal(); if (real != null) { created(real); break; } failed(); if (mMinRetryDelayMillis < 0) { break; } try { Thread.sleep(mMinRetryDelayMillis); } catch (InterruptedException e) { break; } } handleThreadExit(null); } catch (Throwable e) { handleThreadExit(e); } } } private static class TFactory implements ThreadFactory { private static int cCount; private static synchronized int nextID() { return ++cCount; } public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setDaemon(true); t.setName("BelatedCreator-" + nextID()); return t; } } }