Angepassten Evictor schreiben

WebSphere eXtreme Scale ermöglicht Ihnen das Schreiben einer angepassten Bereinigungsimplementierung.

Sie müssen einen angepassten Evictor (Bereinigungsprogramm) erstellen, das die Schnittstelle "Evictor" implementiert, und den allgemeinen Konventionen für eXtreme-Scale-Plug-ins folgen. Im Folgenden sehen Sie die Schnittstelle:
public interface Evictor
{
    void initialize(BackingMap map, EvictionEventCallback callback);
    void activate();
    void apply(LogSequence sequence);
    void deactivate();
    void destroy();
}
Die Schnittstelle "EvictionEventCallback" hat die folgenden Methoden:
public interface EvictionEventCallback
{
    void evictMapEntries(List evictorDataList) throws ObjectGridException;
    void evictEntries(List keysToEvictList) throws ObjectGridException;
    void setEvictorData(Object key, Object data);
    Object getEvictorData(Object key);
}

Die EvictionEventCallback-Methoden werden wie folgt von einem Evictor-Plug-in verwendet, um einen Rückruf an das eXtreme-Scale-Framework zu senden:

eXtreme Scale ruft die Methode "apply" der Evictor-Schnittstelle nach Abschluss einer Transaktion auf. Alle Transaktionssperren, die von der abgeschlossenen Transaktion angefordert wurden, werden freigegeben. Potenziell können mehrere Threads die Methode "apply" gleichzeitig aufrufen, und jeder Thread kann seine eigene Transaktion abschließen. Da Transaktionssperren bereits von der abgeschlossenen Transaktion freigegeben werden, muss die Methode "apply" eine eigene Synchronisation durchführen, um sicherzustellen, dass sie Thread-sicher ist.

Der Grund für die Implementierung der Schnittstelle "EvictorData" und die Verwendung der Methode "evictMapEntries" an Stelle der Methode "evictEntries" ist das Schließen eines potenziellen Zeitfensters. Stellen Sie sich die folgende Abfolge von Ereignissen vor:
  1. Transaktion 1 wird abgeschlossen und ruft die Methode "apply" mit einer LogSequence auf, die den Map-Eintrag für Schlüssel 1 löscht.
  2. Transaktion 2 wird abgeschlossen und ruft die Methode "apply" mit einer LogSequence auf, die einen neuen Map-Eintrag für Schlüssel 1 einfügt. Anders ausgedrückt, Transaktion 2 erstellt den Map-Eintrag erneut, der von Transaktion 1 gelöscht wurde.

Da der Evictor asynchron zu Threads ausgeführt wird, die Transaktionen ausführen, ist es möglich, dass der Evictor, wenn er entscheidet, Schlüssel 1 zu entfernen, den Map-Eintrag entfernt, der vor Abschluss von Transaktion 1 vorhanden war, oder den Map-Eintrag, der von Transaktion 2 erneut erstellt wurde. Um Zeitfenster zu schließen und die Unsicherheit in Bezug darauf auszuschließen, welche Map-Eintragsversion von Schlüssel 1 der Evictor zu entfernen beabsichtigt, implementieren Sie die Schnittstelle "EvictorData" über das Objekt, das an die Methode "setEvictorData" übergeben wird. Verwenden Sie für die gesamte Lebensdauer eines Map-Eintrags dieselbe EvictorData-Instanz. Wenn dieser Map-Eintrag gelöscht und anschließend von einer anderen Transaktion neu erstellt wird, muss der Evictor eine neue Instanz der EvictorData-Implementierung verwenden. Mit Hilfe der EvictorData-Implementierung und der Methode "evictMapEntries" kann der Evictor sicherstellen, dass der Map-Eintrag nur dann entfernt wird, wenn der Cacheeintrag, der dem Map-Eintrag zugeordnet ist, die richtige EvictorData-Instanz enthält.

Die Schnittstellen "Evictor" und "EvictonEventCallback" ermöglichen einer Anwendung, einen Evictor zu integrieren, der einen benutzerdefinierten Algorithmus für die Bereinigung implementiert. Das folgende Code-Snippet veranschaulicht, wie Sie die Methode "initialize" der Schnittstelle "Evictor" implementieren können:
import com.ibm.websphere.objectgrid.BackingMap;
import com.ibm.websphere.objectgrid.plugins.EvictionEventCallback;
import com.ibm.websphere.objectgrid.plugins.Evictor;
import com.ibm.websphere.objectgrid.plugins.LogElement;
import com.ibm.websphere.objectgrid.plugins.LogSequence;
import java.util.LinkedList;
// Instanzvariablen
private BackingMap bm;
private EvictionEventCallback evictorCallback;
private LinkedList queue;
private Thread evictorThread;
public void initialize(BackingMap map, EvictionEventCallback callback)
{
    bm = map;
    evictorCallback = callback;
    queue = new LinkedList();
    // spawn evictor thread
    evictorThread = new Thread( this );
    String threadName = "MyEvictorForMap−" + bm.getName();
    evictorThread.setName( threadName );
    evictorThread.start();
}

Der vorherige Code speichert die Referenzen auf die Map und Callback-Objekte in Instanzvariablen, so dass sie den Methoden "apply" und "destroy" zur Verfügung stehen. In diesem Beispiel wird eine verkettete Liste erstellt, die als FIFO-Warteschlange für die Implementierung eines LRU-Algorithmus verwendet wird. Es wird wird ein Thread gestartet und eine Referenz auf den Thread als Instanzvariable verwaltet. Mit dieser Referenz ist die Methode "destroy" in der Lage, den gestarteten Thread zu unterbrechen und zu beenden.

Das folgende Code-Snippet veranschaulicht, wie die Methode "apply" der Schnittstelle "Evictor" implementiert werden kann. In diesem Code werden die Synchronisationsanforderungen für die Gewährleistung der Thread-Sicherheit des Codes ignoriert:
import com.ibm.websphere.objectgrid.BackingMap;
import com.ibm.websphere.objectgrid.plugins.EvictionEventCallback;
import com.ibm.websphere.objectgrid.plugins.Evictor;
import com.ibm.websphere.objectgrid.plugins.EvictorData;
import com.ibm.websphere.objectgrid.plugins.LogElement;
import com.ibm.websphere.objectgrid.plugins.LogSequence;

public void apply(LogSequence sequence)
{
    Iterator iter = sequence.getAllChanges();
    while ( iter.hasNext() )
    {
        LogElement elem = (LogElement)iter.next();
        Object key = elem.getKey();
        LogElement.Type type = elem.getType();
        if ( type == LogElement.INSERT )
        {
            // Insert-Verarbeitung hier durchführen, indem sie an den Anfang der LRU-Warteschlange gestellt wird
            EvictorData data = new EvictorData(key);
            evictorCallback.setEvictorData(key, data);
            queue.addFirst( data );
        }
        else if ( type == LogElement.UPDATE || type == LogElement.FETCH || type == LogElement.TOUCH )
        {
            // Update-Verarbeitung hier durchführen, indem das EvictorData-Objekt an den Anfang
            // der Warteschlange gestellt wird
                        EvictorData data = evictorCallback.getEvictorData(key);
            queue.remove(data);
            queue.addFirst(data);
        }
        else if ( type == LogElement.DELETE || type == LogElement.EVICT )
        {
            // Remove-Verarbeitung hier durchführung, indem das EvictorData-Objekt aus der
            // Warteschlange entfernt wird
                        EvictorData data = evictorCallback.getEvictorData(key);
            if ( data == EvictionEventCallback.KEY_NOT_FOUND )
            {
                // Die Annahme ist hier, dass der asynchrone Evictor-Thread
                // den Map-Eintrag entfernt hat, bevor dieser Thread die Chance hatte,
                // die LogElement-Anforderung zu verarbeiten. Wahrscheinlich ist in einem
                // solchen Fall nichts zu tun.
                        }
            else
            {
                // Schlüssel wurde gefunden. Also die Daten des Evictors verarbeiten.
                if ( data != null )
                {
                    // Von der Methode "remove" zurückgegebenen Nullwert ignorieren,
                    // da der gestartete Evictor-Thread den Eintrag
                    // bereits aus der Warteschlange entfernt haben kann.
                                        // Dieser Code wird jedoch benötigt, falls der Evictor-Thread
                    // doch nicht für dieses LogElement verantwortlich ist.
                                        queue.remove( data );
                }
                else
                {
                    // Je nachdem, wie Sie ihren Evictor schreiben, ist
                    // diese Möglichkeit entweder nicht vorhanden, oder sie kann
                    // auf einen Fehler in Ihrem Evictor aufgrund
                    // einer ungültigen Logik für die Thread-Synchronisation hinweisen.
                                }
            }
        }
    }
}

Die Insert-Verarbeitung in der Methode "apply" ist gewöhnlich für die Erstellung eines EvictorData-Objekts zuständig, das an die Methode "setEvictorData" der Schnittstelle "EvictionEventCallback" übergeben wird. Da dieser Evictor eine LRU-Implementierung veranschaulicht, wird das EvictorData-Objekt ebenfalls am Anfang der Warteschlange hinzugefügt, die von der Methode "initialize" erstellt wurde. Die Update-Verarbeitung in der Methode "apply" aktualisiert gewöhnlich das EvictorData-Objekt, das von einem vorherigen Aufruf der Methode "apply" erstellt wurde (z. B. von der Insert-Verarbeitung der Methode "apply"). Da dieser Evictor eine LRU-Implementierung ist, muss er das EvictorData-Objekt von der aktuellen Warteschlangenposition an den Anfang der Warteschlange verschieben. Der gestartete Evictor-Thread entfernt das letzte EvictorData-Objekt in der Warteschlange, weil das letzte Warteschlangenelement den Eintrag mit dem ältesten Verwendungsdatum entfernt. Die Annahme ist, dass das EvictorData-Objekt eine Methode "getKey" enthält, so dass der Evictor-Thread die Schlüssel der Einträge kennt, die entfernt werden müssen. Denken Sie daran, dass in diesem Beispiel die Synchronisationsanforderungen für die Thread-Sicherheit ignoriert werden. Ein echter angepasster Evictor ist viel komplexer, weil er sich um die Synchronisation und Leistungsengpässe kümmern muss, die an den Synchronisationspunkten auftreten.

Die folgenden Code-Snippets veranschaulichen die Methode "destroy" und Methode "run" des ausführbaren Threads, den die Methode "initialize" gestartet hat:
// Die Methode "destroy" unterbricht einfach den von der Methode "initialize" gestarteten Thread
public void destroy()
{
        evictorThread.interrupt();
}

// Es folgt die Methode "run" des Threads, der von der Methode "initialize" gestartet wurde
public void run()
{
    // Schleife, bis die Methode "destroy" diesen Thread unterbricht
    boolean continueToRun = true;
    while ( continueToRun )
    {
        try
        {
            // Eine Weile inaktiv bleiben, bevor die Warteschlange bereinigt wird.
            // sleepTime eignet sich bestens als Eigenschaft, die für einen
            // Evictor gesetzt werden kann.
                        Thread.sleep( sleepTime );
            int queueSize = queue.size();
            // Einträge entfernen, wenn die Warteschlangengröße die definierte
            // Maximalgröße überschreitet. Die maximale Größe ist also
            // ebenfalls eine Eigenschaft des Evictors.
                        int numToEvict = queueSize − maxSize;
            if ( numToEvict > 0 )
            {
                // Eintrag am Ende der Warteschlange entfernen, da sich
                // dort der Eintrag mit dem ältesten Verwendungsdatum befindet
                                List evictList = new ArrayList( numToEvict );
                while( queueSize > ivMaxSize )
                {
                    EvictorData data = null;
                    try
                    {
                        EvictorData data = (EvictorData) queue.removeLast();
                        evictList.add( data );
                        queueSize = queue.size();
                    }
                    catch ( NoSuchElementException nse )
                    {
                        // Warteschlange ist leer
                        queueSize = 0;
                    }
                }
                // Bereinigung anfordern, wenn die Schlüsselliste nicht leer ist
                if ( ! evictList.isEmpty() )
                {
                    evictorCallback.evictMapEntries( evictList );
                }
            }
        }
        catch ( InterruptedException e )
        {
            continueToRun = false;
        }
    } // end while loop
} // Ende der Methode "run"

Optionale Schnittstelle "RollBackEvictor"

Die Schnittstelle "com.ibm.websphere.objectgrid.plugins.RollbackEvictor" kann von einem Evictor-Plug-in optional implementiert werden. Wenn diese Schnittstelle implementiert wird, kann ein Evictor nicht nur aufgerufen werden, wenn Transaktionen festgeschrieben werden, sondern auch, wenn Transaktionen rückgängig gemacht werden (Rollback).
public interface RollbackEvictor
{
    void rollingBack( LogSequence ls );
}

Die Methode "apply" wird nur aufgerufen, wenn eine Transaktion festgeschrieben wird. Wenn eine Transaktion rückgängig gemacht wird und die Schnittstelle "RollbackEvictor" vom Evictor implementiert wird, wird eine Methode "rollingBack" aufgerufen. Wird die Schnittstelle "RollbackEvictor" nicht implementiert und die Transaktion rückgängig gemacht, werden die Methoden "apply" und "rollingBack" nicht aufgerufen.