Gravando um Evictor Customizado

WebSphere eXtreme Scale permite que você grave uma implementação de despejo customizada.

É necessário criar um evictor customizado que implemente a interface do evictor e siga as convenções de plug-in do eXtreme Scale comuns. A interface é a seguinte:
public interface Evictor
{
    void initialize(BackingMap map, EvictionEventCallback callback);
    void activate();
    void apply(LogSequence sequence);
    void deactivate();
    void destroy();
}
A interface EvictionEventCallback tem os seguintes métodos:
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);
}

Os métodos EvictionEventCallback são usados por um plug-in Evictor para retornar à estrutura eXtreme Scale como a seguir:

Após uma transação ser concluída, o eXtreme Scale chama o método apply da interface do Evictor. Todos os bloqueios de transação que foram adquiridos pela transação concluída não estão mais suspensos. Provavelmente, vários encadeamentos podem chamar o método apply ao mesmo tempo e cada encadeamento pode concluir sua própria transação. Como os bloqueios de transação já foram liberados pela transação concluída, o método apply deve fornecer sua própria sincronização para assegurar que o método apply seja seguro em encadeamento.

A razão para implementar a interface EvictorData e utilizar o método evictMapEntries em vez do método evictEntries é fechar uma possível janela de cronometragem. Considere a seguinte sequência de eventos:
  1. A transação 1 é concluída e chama o método apply com uma LogSequence que exclui a entrada do mapa para a chave 1.
  2. A transação 2 é concluída e chama o método apply com uma LogSequence que insere uma nova entrada do mapa para a chave 1. Em outras palavras, a transação 2 recria a entrada do mapa que foi excluída pela transação 1.

Como o evictor é executado assincronicamente a partir de encadeamentos que executam transações, é possível que quando o evictor decida liberar a chave 1, ele possa liberar a entrada do mapa existente antes da conclusão da transação 1 ou possa liberar a entrada do mapa que foi recriada pela transação 2. Para eliminar janelas de cronometragens e eliminar a incerteza quanto à qual versão da entrada do mapa da chave 1 o evictor pretendia liberar, implemente a interface EvictorData pelo objeto transmitido ao método setEvictorData. Utilize a mesma instância EvictorData durante a existência de uma entrada do mapa. Quando essa entrada do mapa for excluída e, em seguida, recriada por outra transação, o evictor deverá utilizar uma nova instância da implementação de EvictorData. Utilizando a implementação de EvictorData e utilizando o método evictMapEntries, o evictor pode assegurar que a entrada do mapa seja liberada apenas se a entrada de cache associada à entrada do mapa contiver a instância EvictorData correta.

As interfaces Evictor e EvictonEventCallback permitem que um aplicativo conecte um evictor que implementa um algoritmo definido pelo usuário para evicção. O fragmento de código a seguir ilustra como é possível implementar o método initialize da interface Evictor:
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;
// Instance variables
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 do encadeamento do evictor
    evictorThread = new Thread( this );
    String threadName = "MyEvictorForMap−" + bm.getName();
    evictorThread.setName( threadName );
    evictorThread.start();
}

O código anterior salva as referências ao mapa e objetos de retorno de chamada em variáveis da instância para que fiquem disponíveis para os métodos apply e destroy. Neste exemplo, uma lista vinculada é criada para utilização como uma fila de primeiro a entrar e primeiro a sair para implementação de um algoritmo LRU (Least Recently Used). É efetuado spawn de um encadeamento e uma referência ao encadeamento é mantida como uma variável de instância. Mantendo esta referência, o método destroy pode interromper e terminar o encadeamento no qual foi efetuado spawn.

Ignorando os requisitos de sincronização para tornar um código seguro em encadeamento, o trecho de código a seguir ilustra como o método apply da interface Evictor pode ser implementado:
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 )
        {
            // do insert processing here by adding to front of LRU queue.
            EvictorData data = new EvictorData(key);
            evictorCallback.setEvictorData(key, data);
            queue.addFirst( data );
        }
        else if ( type == LogElement.UPDATE || type == LogElement.FETCH || type == LogElement.TOUCH )
        {
            // do update processing here by moving EvictorData object to
            // front of queue.
            EvictorData data = evictorCallback.getEvictorData(key);
            queue.remove(data);
            queue.addFirst(data);
        }
        else if ( type == LogElement.DELETE || type == LogElement.EVICT )
        {
            // do remove processing here by removing EvictorData object
            // from queue.
            EvictorData data = evictorCallback.getEvictorData(key);
            if ( data == EvictionEventCallback.KEY_NOT_FOUND )
            {
                // Assumption here is your asynchronous evictor thread
                // evicted the map entry before this thread had a chance
                // to process the LogElement request. So you probably
                // need to do nothing when this occurs.
            }
            else
            {
                // Key was found. So process the evictor data.
                if ( data != null )
                {
                    // Ignore null returned by remove method since spawned
                    // evictor thread may have already removed it from queue.
                    // But we need this code in case it was not the evictor
                    // thread that caused this LogElement to occur.
                    queue.remove( data );
                }
                else
                {
                    // Depending on how you write you Evictor, this possibility
                    // may not exist or it may indicate a defect in your evictor
                    // due to improper thread synchronization logic.
                }
            }
        }
    }
}

O processamento de inserção no método apply geralmente manipula a criação de um objeto de dados do evictor transmitido para o método setEvictorData da interface EvictionEventCallback. Como este evictor ilustra a implementação de um LRU, o EvictorData também é incluído na frente da fila que foi criada pelo método initialize. O processamento de atualização no método apply geralmente atualiza o objeto de dados do evictor criado por alguma chamada anterior do método apply (por exemplo, pelo processamento de inserção do método apply). Como este evictor é a implementação de um LRU, ele precisa mover o objeto EvictorData de sua posição de fila atual para a frente da fila. O encadeamento do evictor no qual foi efetuado spawn remove o último objeto EvictorData na fila, porque o último elemento da fila representa a entrada least recently used. A suposição é que o objeto EvictorData contenha um método getKey para que o encadeamento do evictor saiba quais são as chaves das entradas que precisam ser liberadas. Observe que este exemplo está ignorando os requisitos de sincronização para tornar o código seguro em encadeamento. Um evictor customizado real é mais complicado, porque lida com sincronização e gargalos de desempenho que ocorrem como resultado dos pontos de sincronização.

Os seguintes trechos de código ilustram o método destroy e o método run do encadeamento executável no qual foi efetuado spawn pelo método initialize:
// Destroy method simply interrupts the thread spawned by the initialize method.
public void destroy()
{
    evictorThread.interrupt();
}

// Here is the run method of the thread that was spawned by the initialize method.
public void run()
{
    // Loop until destroy method interrupts this thread.
    boolean continueToRun = true;
    while ( continueToRun )
    {
        try
        {
            // Sleep for a while before sweeping over queue.
            // The sleepTime is a good candidate for a evictor
            // property to be set.
            Thread.sleep( sleepTime );
            int queueSize = queue.size();
            // Evict entries if queue size has grown beyond the
            // maximum size. Obviously, maximum size would
            // be another evictor property.
            int numToEvict = queueSize − maxSize;
            if ( numToEvict > 0 )
            {
                // Remove from tail of queue since the tail is the
                // least recently used entry.
                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 )
                    {
                        // The queue is empty.
                        queueSize = 0;
                    }
                }
                // Request eviction if key list is not empty.
                if ( ! evictList.isEmpty() )
                {
                    evictorCallback.evictMapEntries( evictList );
                }
            }
        }
        catch (InterruptedException e)
        {
            continueToRun = false;
        }
    } // end while loop
} // end run method.

Interface RollBackEvictor Opcional

A interface com.ibm.websphere.objectgrid.plugins.RollbackEvictor pode ser opcionalmente implementada por um plug-in Evictor. Implementando esta interface, um evictor pode ser chamado não apenas quando as transações forem confirmadas, mas também quando for efetuado rollback das transações.
public interface RollbackEvictor
{
    void rollingBack( LogSequence ls );
}

O método apply será chamado apenas de uma transação for confirmada. Se for efetuado rollback de uma transação e a interface RollbackEvictor for implementada pelo evictor, o método rollingBack será chamado. Se a interface RollbackEvictor não for implementada e for efetuado rollback da transação, o método apply e o método rollingBack não serão chamados.