[Java programming language only]

Custom evictors

You can write a custom eviction implementation. Custom evictors are not supported in environments that have IBM® eXtremeMemory configured.

Create a custom evictor that implements the evictor interface and follows the common eXtreme Scale plug-in conventions. The interface follows:
public interface Evictor
{
    void initialize(BackingMap map, EvictionEventCallback callback);
    void activate();
    void apply(LogSequence sequence);
    void deactivate();
    void destroy();
}
  • The initialize method is started during initialization of the BackingMap object. This method initializes an Evictor plug-in with a reference to the BackingMap and a reference to an object that implements the com.ibm.websphere.objectgrid.plugins.EvictionEventCallback interface.
  • The activate method is called to activate the Evictor. After this method is called, the Evictor can use the EvictionEventCallback interface to evict map entries. If the Evictor attempts to use the EvictionEventCallbackinterface to evict map entries before the activate method is called, an IllegalStateException exception results.
  • The apply method is started when transactions that access one or more entries of the BackingMap are committed. The apply method is passed a reference to an object that implements the com.ibm.websphere.objectgrid.plugins.LogSequence interface. The LogSequence interface allows an Evictor plug-in to determine which BackingMap entries were created, modified, or removed by the transaction. An Evictor uses this information in deciding when and which entries to evict.
  • The deactivate method is called to deactivate the Evictor. After this method is called, the Evictor must stop using the EvictionEventCallback interface to evict map entries. If the Evictor uses the EvictionEventcallback interface after this method is called, an IllegalStateException exception results.
  • The destroy method is run when the BackingMap is being removed. This method allows an Evictor to remove any threads that it created.
The EvictionEventCallback interface has the following methods:
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);
}

The EvictionEventCallback methods are used by an Evictor plug-in to call back to the eXtreme Scale framework as follows:

  • The setEvictorData method is used by an evictor to request the framework that is used to store and associate some evictor object that it creates with the entry indicated by the key argument. The data is evictor specific and is determined by the information the evictor needs to implement the algorithm it is using. For example, in a least frequently used algorithm, the evictor maintains a count in the evictor data object for tracking how many times the apply method is invoked with a LogElement that refers to an entry for a given key.
  • The getEvictorData method is used by an evictor to retrieve the data it passed to the setEvictorData method during a prior apply method invocation. If evictor data for the specified key argument is not found, a special KEY_NOT_FOUND object that is defined on the EvictorCallback interface is returned.
  • The evictMapEntries method is used by an evictor to request the eviction of one or more map entries. Each object in the evictorDataList parameter must implement the com.ibm.websphere.objectgrid.plugins.EvictorData interface. Also, the same EvictorData instance that is passed to the setEvictorData method must be in the evictor data list parameter of this method. The getKey method of the EvictorData interface is used to determine which map entry to evict. The map entry is evicted if the cache entry currently contains the exact same EvictorData instance that is in the evictor data list for this cache entry.
  • The evictEntries method is used by an evictor to request eviction of one or more map entries. This method is used only if the object that is passed to the setEvictorData method does not implement the com.ibm.websphere.objectgrid.plugins.EvictorData interface.

After a transaction completes eXtreme Scale calls the apply method of the Evictor interface. All transaction locks that were acquired by the completed transaction are no longer held. Potentially, multiple threads can call the apply method at the same time, and each thread can complete its own transaction. Because transaction locks are already released by the completed transaction, the apply method must provide its own synchronization to ensure the apply method is thread safe.

The reason to implement the EvictorData interface and use the evictMapEntries method instead of the evictEntries method is to close a potential timing window. Consider the following sequence of events:
  1. Transaction 1 completes and calls the apply method with a LogSequence that deletes the map entry for key 1.
  2. Transaction 2 completes and calls the apply method with a LogSequence that inserts a new map entry for key 1. In other words, transaction 2 re-creates the map entry that was deleted by transaction 1.

Because the evictor runs asynchronously from threads that run transactions, it is possible that when the evictor decides to evict key 1, it might be evicting either the map entry that existed before transaction 1 completion, or it might be evicting the map entry that was re-created by transaction 2. To eliminate possible delays and to eliminate uncertainty as to which version of the key one map entry the evictor that is intended to evict, implement the EvictorData interface by the object that is passed to the setEvictorData method. Use the same EvictorData instance for the life of a map entry. When that map entry is deleted and is then re-created by another transaction, the evictor must use a new instance of the EvictorData implementation. By using the EvictorData implementation and by using the evictMapEntries method, the evictor can ensure that the map entry is evicted if and only if the cache entry that is associated with the map entry contains the correct EvictorData instance.

The Evictor and EvictonEventCallback interfaces allow an application to plug in an evictor that implements a user-defined algorithm for eviction. The following snippet of code illustrates how you can implement the initialize method of Evictor interface:
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 evictor thread
    evictorThread = new Thread( this );
    String threadName = "MyEvictorForMap−" + bm.getName();
    evictorThread.setName( threadName );
    evictorThread.start();
}

The preceding code saves the references to the map and callback objects in instance variables so that they are available to the apply and destroy methods. In this example, a linked list is created that is used as a first in, first out queue for implementing a least recently used (LRU) algorithm. A thread is created and a reference to the thread is kept as an instance variable. By keeping this reference, the destroy method can interrupt and remove the created thread.

Ignoring synchronization requirements to make code thread safe, the following snippet of code illustrates how the apply method of the Evictor interface can be implemented:
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.
                }
            }
        }
    }
}

Insert processing in the apply method typically handles the creation of an evictor data object that is passed to the setEvictorData method of the EvictionEventCallback interface. Because this evictor illustrates an LRU implementation, the EvictorData is also added to the front of the queue that was created by the initialize method. Update processing in the apply method typically updates the evictor data object that was created by some prior invocation of the apply method (for example, by the insert processing of the apply method). Because this evictor is an LRU implementation, it must move the EvictorData object from its current queue position to the front of the queue. The spawned evictor thread removes the last EvictorData object in the queue because the last queue element represents the least recently used entry. The assumption is that the EvictorData object has a getKey method on it so that the evictor thread knows the keys of the entries that must be evicted. Keep in mind that this example is ignoring synchronization requirements to make code thread safe. A real custom evictor is more complicated because it deals with synchronization and performance bottlenecks that occur as a result of the synchronization points.

The following snippets of code illustrate the destroy method and the run method of the runnable thread that the initialize method created:
// 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.

Optional RollBackEvictor interface

The com.ibm.websphere.objectgrid.plugins.RollbackEvictor interface can be optionally implemented by an Evictor plug-in. By implementing this interface, an evictor can be run not only when transactions are committed, but also when transactions are rolled back.
public interface RollbackEvictor
{
    void rollingBack( LogSequence ls );
}

The apply method is called only if a transaction is committed. If a transaction is rolled back and the RollbackEvictor interface is implemented by the evictor, the rollingBack method is run. If the RollbackEvictor interface is not implemented and the transaction rolls back, the apply method and the rollingBack method are not called.