~/home of geeks

Borgen und Sorgen

· 1064 Wörter · 5 Minute(n) Lesedauer

Neulich hatte ich einen “Concurrent Modification Leak”, der durch das weitere Benutzen von Objekten aus einem Pool passierte, welche dem Pool bereits zurückgegeben wurden.

Leiht man sich ein Objekt aus einem Pool, und gibt dieses wieder zurück, sollte man keine Referenz mehr darauf haben und das Objekt auch nicht mehr benutzen, da andere Leiher in der Zwischenzeit ebenfalls Zugriff auf das gleiche Objekt haben könnten und damit der Zustand von verschiedenen Stellen manipuliert werden kann.

Leider hatte ich nun eine Situation, in der ich vermuten konnte, dass genau dieses passiert, aber nicht wo.

Ursprünglich hatte ich Apache Commons Pool in Verwendung, dass mir aber nicht sagen konnte, wo auf ein Objekt zugegriffen wurde, das dem Pool bereits zurückgegeben wurde.

Kurzerhand schrieb ich einen einfachen kleinen Pool, der alle seine Objekte mit einem CGLib-Proxy ummantelt und so nachhält, ob ein Objekt benutzt wird, obwohl es zurückgegeben wurde.

Der Pool hat nicht viele Features, er kann bei Bedarf, also wenn ihm die Objekte ausgehen, über eine Factory neue Objekte erzeugen. Diese werden auch direkt mit einem Proxy versehen, welcher wiederum vom Pool gesteuert wird. So setzt der Pool bei Herausgabe eines Objektes im Proxy die Verwendbarkeit auf “aktiv”, wodurch alle Aufrufe am Proxy weiter delegiert werden. Bei Rückgabe an den Pool wird die Verwendbarkeit “deaktiviert”. Dies führt dazu, dass jeglicher Aufruf am Proxy zu einer Exception samt Stacktrace führt.

Auf diese Weise konnte ich die leakende Stelle ermitteln und reparieren. Ich habe diesen Pool dann auch direkt im Code gelassen, da es sich sowieso um Testklassen handelte und der Pool nach Ablauf der Tests verworfen wird.

Es sei aber noch darauf hingewiesen, dass der ganze Mechanismus nicht immer funktioniert, denn ein Objekt, das zurückgegeben und bereits erneut verliehen wurde, wird keine Exception auslösen, wenn es an der alten Stelle weiterhin benutzt wird. In meinem Fall gab es jedoch genug Aufrufe, um eine Exception zu erzeugen.

/**
 * Eine Schnittstelle zum Erstellen von neuen Objekten
 * für einen Pool.
 * 
 * @author Serhat Cinar
 */
public interface IObjectFactory<T> {
    public T create();
}
/**
 * Eine Schnittstelle zum Aufbereiten von Objekten
 * aus einem Pool, bevor sie herausgegeben werden.
 * Hier können Objekte vorkonfiguriert werden, damit sie den Anforderungen entsprechen.
 */
public interface IObjectPreparator<T> {
    public T prepare(T obj);
}
import java.lang.reflect.Method;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

/**
 * Interceptor, der prüft, dass nicht ausgeliehene bzw. zurückgegebene Objekte nicht mehr benutzt werden.
 * Verhindert hier durch leakende Poolobjekte, also solche, die zurückgegeben wurden, aber irgendwo noch referenziert und weiter verwendet werden.
 */
public class PoolObjectMethodInterceptor implements MethodInterceptor {
    private static final Logger LOG = LoggerFactory.getLogger(PoolObjectMethodInterceptor.class);
    
    private final Object poolObject;
    private boolean borrowed = false;

    public PoolObjectMethodInterceptor(final Object poolObject) {
        this.poolObject = poolObject;
    }

    /**
     * @return the borrowed
     */
    public final boolean isBorrowed() {
        return borrowed;
    }

    /**
     * @param borrowed the borrowed to set
     */
    public final void setBorrowed(final boolean borrowed) {
        this.borrowed = borrowed;
    }

    @Override
    public Object intercept(final Object obj, final Method method, final Object[] args, final MethodProxy proxy) throws Throwable {
        LOG.debug("Aufruf {}, {}", poolObject, method.toGenericString());
        if (method.getName().startsWith("set") || method.getName().startsWith("get") || method.getName().startsWith("is") || method.getName().startsWith("add")
            || method.getName().startsWith("remove")) {
            if (!isBorrowed()) {
                throw new RuntimeException("Object " + poolObject + " was used (" + method.getName() + "), but is not (or no more) borrowed by anyone.");
            }
        }
        return method.invoke(poolObject, args);
    }
}
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.cglib.proxy.Enhancer;

/**
 * Klasse zum Poolen von wiederverwertbaren Objekten.
 * Dieser Pool ist Threadsafe. <i>Die gelieferten Objekte könnten nicht Threadsafe sein</i>. <br>
 * Zusätzlich bietet der Pool die Möglichkeit, das Erzeugen {@link IObjectFactory} der Objekte sowie deren Initialisierung bzw. Vorbereitung
 * {@link IObjectPreparator} vor einem Ausleihprozess per separater Klassen zu definieren.
 */
public class ConcurrentObjectPool<T> {
    private static final Logger LOG = LoggerFactory.getLogger(ConcurrentObjectPool.class);

    // Der Pool der Objekte
    private final LinkedList<T> pool = new LinkedList<>();
    
    // Diese Lock sichert die Zugriffe auf diese Klasse in Multithreaded-Umgebungen ab.
    private final ReentrantLock lock = new ReentrantLock(true);
    
    // Factory zum Erzeugen neuer Objekte
    private IObjectFactory<T> objectFactory;
    
    // Zum Aufbereiten von Objekten vor dem Ausleihen
    private IObjectPreparator<T> objectPreparator;
    
    // Interceptor zum Absichern, dass zurückgegebene Objekte nicht mehr benutzt werden
    private final Map<T, PoolObjectMethodInterceptor> methodInterceptors = new HashMap<>();

    /**
     * Legt eine Klasse fest, welche neue Objekte erzeugt, wenn der Pool Bedarf an neuen Objekten hat.
     * 
     * @param objectFactory the objectFactory to set
     */
    @Required
    public final void setObjectFactory(final IObjectFactory<T> objectFactory) {
        this.objectFactory = objectFactory;
    }

    /**
     * Legt eine Klasse fest, welche die Objekte vor der Herausgabe in {@link #borrow()}
     * bearbeitet. Dies ist sinnvoll, wenn Objekte einen initial-Zustand haben sollen.
     * Dies kann im {@link IObjectPreparator} durchgeführt werden.<br/>
     * Optional.
     *    
     * @param objectPreparator the objectPreparator to set
     */
    public final void setObjectPreparator(final IObjectPreparator<T> objectPreparator) {
        this.objectPreparator = objectPreparator;
    }

    private LinkedList<T> getPool() {
        return pool;
    }

    /**
     * Erzeugt ein neues Objekt für den Pool.
     */
    @SuppressWarnings("unchecked")
    private void generateNewObject() {
        // Lockabsicherung, falls mal ein Entwickler diese Methode aus einer ungesicherten Methode aus aufruft.
        lock.lock();
        try {
            final T newObject = objectFactory.create();
            final PoolObjectMethodInterceptor interceptor = new PoolObjectMethodInterceptor(newObject);
            interceptor.setBorrowed(true);
            T proxiedNewObject = null;
            proxiedNewObject = (T) Enhancer.create(newObject.getClass(), interceptor);
            methodInterceptors.put(proxiedNewObject, interceptor);
            LOG.debug("Erzeuge {}", proxiedNewObject);
            getPool().add(proxiedNewObject);
            interceptor.setBorrowed(false);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Leiht ein Objekt aus. Das Objekt muss per {@link #release(Object)} wieder dem Pool zugeführt werden.
     * Thread-safe.
     * 
     * @return
     */
    public final T borrow() {
        T result = null;
        lock.lock();
        try {
            if (getPool().isEmpty()) {
                generateNewObject();
            }
            result = getPool().removeFirst();

            final PoolObjectMethodInterceptor interceptor = methodInterceptors.get(result);
            interceptor.setBorrowed(true);

            if (objectPreparator != null) {
                result = objectPreparator.prepare(result);
            }
            LOG.debug("Abruf {}", result);
            return result;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Gibt ein ausgeliehenes Objekt zurück/frei.<br/>
     * Objekte, die nicht aus diesem Pool stammen, werden nicht aufgenommen (werden ignoriert).
     * Thread-safe.
     * 
     * @param t
     */
    public final void release(final T t) {
        lock.lock();
        try {
            final PoolObjectMethodInterceptor interceptor = methodInterceptors.get(t);
            // Nur Objekte aus diesem Pool mit richtigem Interceptor.
            if (interceptor != null) {
                if (!getPool().contains(t)) {
                    getPool().add(t);
                }
                interceptor.setBorrowed(false);
            }
            LOG.debug("Rückgabe {}", t);
        } finally {
            lock.unlock();
        }
    }
}

Anwendung:

ConcurrentObjectPool<MyObject> pool = new ConcurrentObjectPool<>();
pool.setObjectFactory(
  new IObjectFactory<MyObject>(){
    @Override
    public MyObject create(){
      // Erzeugen neuer Poolobjekte
      return new MyObject();
    }
  }
);
pool.setObjectPreparator(
  new ObjectPreparator<MyObject>(){
    @Override
    public MyObject prepare(MyObject obj){
      // Initialisieren vor erneutem Einsatz
      obj.init();
    }
  }
);
MyObject poolObject = pool.borrow();
try{
  // ...
}
finally{
  pool.release(poolObject);
}