~pperalta

Thoughts on software development and other stuff

Coherence Key HOWTO

with 14 comments

Image credit: Brenda Starr

On occasion I am asked about best practices for creating classes to be used as keys in Coherence. This usually comes about due to unexpected behavior that can be explained by incorrect key implementations.

First and foremost, equals and hashCode need to be implemented correctly for any type used as a key. I won’t describe how to do this – instead I’ll defer to Josh Bloch who has written the definitive guide on this topic.

There is an additional requirement that needs to be addressed. All serializable (non transient) fields in the key class must be used in the equals implementation. To understand this requirement, let’s explore how Coherence works behind the scenes.

First, let’s try the following experiment:

public class Key
        implements Serializable
    {
    public Key(int id, String zip)
        {
        m_id = id;
        m_zip = zip;
        }
 
    //...
 
    @Override
    public boolean equals(Object o)
        {
        // print stack trace
        new Throwable("equals debug").printStackTrace();
        if (this == o)
            {
            return true;
            }
        if (o == null || getClass() != o.getClass())
            {
            return false;
            }
 
        Key key = (Key) o;
 
        if (m_id != key.m_id)
            {
            return false;
            }
        if (m_zip != null ? !m_zip.equals(key.m_zip) : key.m_zip != null)
            {
            return false;
            }
        return true;
        }
 
    @Override
    public int hashCode()
        {
        // print stack trace
        new Throwable("hashCode debug").printStackTrace();        
        int result = m_id;
        result = 31 * result + (m_zip != null ? m_zip.hashCode() : 0);
        return result;
        }
 
    private int m_id;
    private String m_zip;
    }

This key prints out stack traces in equals and hashCode. Now use this key with a HashMap:

public static void testKey(Map m)
    {
    Key key = new Key(1, "12345");
 
    m.put(key, "value");
    m.get(key);
    }
 
//...
 
testKey(new HashMap());

Output is as follows:

java.lang.Throwable: hashCode debug
	at oracle.coherence.idedc.Key.hashCode(Key.java:60)
	at java.util.HashMap.put(HashMap.java:372)
	at oracle.coherence.idedc.KeyTest.testKey(KeyTest.java:46)
	at oracle.coherence.idedc.KeyTest.testKey(KeyTest.java:52)
	at oracle.coherence.idedc.KeyTest.main(KeyTest.java:18)
java.lang.Throwable: hashCode debug
	at oracle.coherence.idedc.Key.hashCode(Key.java:60)
	at java.util.HashMap.get(HashMap.java:300)
	at oracle.coherence.idedc.KeyTest.testKey(KeyTest.java:47)
	at oracle.coherence.idedc.KeyTest.testKey(KeyTest.java:52)
	at oracle.coherence.idedc.KeyTest.main(KeyTest.java:18)

Try it again with a partitioned cache this time:

testKey(CacheFactory.getCache("dist-test"));

Note the absence of stack traces this time. Does this mean Coherence is not using the key’s equals and hashCode? The short answer (for now) is yes. Here is the flow of events that occur when executing a put with a partitioned cache:

  1. Invoke NamedCache.put
  2. Key and value are serialized
  3. Hash is executed on serialized key to determine which partition the key belongs to
  4. Key and value are transferred to the storage node (likely over the network)
  5. Cache entry is placed into backing map in binary form

Note that objects are not deserialized before placement into the backing map – objects are stored in their serialized binary format. As a result, this means that two keys that are equal to each other in object form must be equal to each other in binary form so that the keys can be later be used to retrieve entries from the backing map. The most common way to violate this principle is to exclude non transient fields from equals. For example:

public class BrokenKey
        implements Serializable
    {
    public BrokenKey(int id, String zip)
        {
        m_id = id;
        m_zip = zip;
        }
 
    //...
 
    @Override
    public boolean equals(Object o)
        {
        if (this == o)
            {
            return true;
            }
        if (o == null || getClass() != o.getClass())
            {
            return false;
            }
 
        BrokenKey brokenKey = (BrokenKey) o;
 
        if (m_id != brokenKey.m_id)
            {
            return false;
            }
 
        return true;
        }
 
    @Override
    public int hashCode()
        {
        int result = m_id;
        result = 31 * result;
        return result;
        }
    }

Note this key has two fields (id and zip) but it only uses id in the equals/hashCode implementation. I have the following method to test this key:

public static void testBrokenKey(Map m)
    {
    BrokenKey keyPut = new BrokenKey(1, "11111");
    BrokenKey keyGet = new BrokenKey(1, "22222");
 
    m.get(keyPut);
    m.put(keyPut, "value");
 
    System.out.println(m.get(keyPut));
    System.out.println(m.get(keyGet));
    }

Output using HashMap:

value
value

Output using partitioned cache:

value
null

This makes sense, since keyPut and keyGet will serialize to different binaries. However, things get really interesting when combining partitioned cache with a near cache. Running the example using a near cache gives the following results:

value
value

What happened? In this case, the first get resulted in a near cache miss, resulting in a read through to the backing partitioned cache. The second get resulted in a near cache hit because the object’s equals/hashCode was used (since near caches store data in object form.)

In addition to equals/hashCode, keep the following in mind:

  • Keys should be immutable. Modifying a key while it is in a map generally isn’t a good idea, and it certainly won’t work in a distributed/partitioned cache.
  • Key should be as small as possible. Many operations performed by Coherence assume that keys are very light weight (such as the key based listeners that are used for near cache invalidation.)
  • Built in types (String, Integer, Long, etc) fit all of this criteria. If possible, consider using one of these existing classes.)

Written by Patrick Peralta

June 6th, 2010 at 10:28 pm

14 Responses to 'Coherence Key HOWTO'

Subscribe to comments with RSS or TrackBack to 'Coherence Key HOWTO'.

  1. Hi Patrick,

    Very interesting and clean explanation. I just need some clarifications from the above explanationa.

    1,Hash is executed on serialized key to determine which partition the key belongs to
    2,Key and value are transferred to the storage node (likely over the network)
    3,Cache entry is placed into backing map in binary form

    What/How exactly the 1st statement works ? Hash is executed on the Domain Object/Converted Binary Object ?

    Key and Value are transferred to the storage nodes in Serialized form/ Binary form ?
    What my understanding is When you issue a NamedCache.put() as a TCP-Extend Client the key and values gets converted to Binary and then passed to TCP-Extend and form there it goes to Sotrage nodes.

    Please correct my understanding if it is wrong.

    Regards
    Amar

    Amar

    10 Jun 10 at 12:00 pm

  2. Hi Amar,

    Here is a simplified way to look at it:

    First thing client does is serialize the key similar to this:

    Binary bin = ExternalizableHelper.toBinary(key);

    Once we have the binary, all subsequent operations by partitioned/distributed cache are performed on this binary; i.e. bin.hashCode()

    In the case of Extend, the client will serialize the key and value, send it to the proxy, and the proxy then passes it along to the storage member that holds the partition for that key.

    Patrick Peralta

    10 Jun 10 at 5:34 pm

  3. One more question.

    How the hashCode() of the KEY is used to decide which patiotion it belongs to/it goes to ? what is the internal logic used here ?

    Regards
    Amar

    Amar

    11 Jun 10 at 4:37 am

  4. Hi Amar,

    We mod the hash code by the number of partitions, which then gives us the partition id.

    Patrick Peralta

    14 Jun 10 at 5:54 pm

  5. I have a question. I have a composite cache key similar to yours. Like in your example I use only one member variable on both equals() and hashcode(). But all my puts and gets (store and lookups) set the second member variable of the key to null. So though I don’t follow the best practices the cache shouldn’t have any issues as the serialized version of the key when it was used to do the put operation would be equal to the serialized version of the key when it gets used on the get operation. Now you might ask why we have the other member variable that is not transient but is not considered on either equals or hashCode.
    We construct the key with the second member variable set and first member variable set to null to force a read through to the database (read write partitioned map) as we don’t have another cache which truly has key as the second member variable.
    So we have
    Key k = new Key(null,”m2″);
    Val v = cache.get(k); //this forces a read through and populates k

    If we find it we remove it immediately and construct the right key to put hte value back
    if (v != null)
    cache.remove(k);
    cache.put(new Key(“m1″,null), v);

    We keep experiencing problems during rolling restart when it can’t find objects that were just put in the cache. Can you please tell us why this might cause problems

    Kasturi

    20 Jul 10 at 11:25 pm

  6. Hi Kasturi,

    It sounds like there are a lot of moving parts here. Based on the snippet you provided I don’t know why reads would return null unexpectedly after a rolling restart. I would suggest submitting a service request to Oracle support, as you would be able to upload your configuration and source code for further analysis.

    Patrick Peralta

    21 Jul 10 at 9:36 am

  7. Hi Patrick
    I am working over implementing key association between Cache key and command context identifier where key operation gets executed. So i have added transient contextIdentifier to my cache key. I am able to insert the data but i am getting null when i try to retrieve the data using same key.If i remove transient contextIdentifier it works fine.I have not added transient field in hashcode ,equals , readexternal,writeexternal, This should not be problem as the field is transient.

    My Cache key implementation is
    public class TradeCacheKey implements PortableObject,KeyAssociation{
    private String tradeId;
    transient private Identifier contextIdentifier;

    public TradeCacheKey()
    {

    }

    /**
    * constructor to create TradecacheKey
    * @param tradeId – tradeId of Trade
    * @param contextIdentifier – context Identifier
    */
    public TradeCacheKey(String tradeId, Identifier contextIdentifier) {

    this.tradeId = tradeId;
    this.contextIdentifier = contextIdentifier;
    }

    public TradeCacheKey(String tradeId) {

    this.tradeId = tradeId;

    }

    @Override
    public void readExternal(PofReader pofreader) throws IOException {
    // TODO Auto-generated method stub
    tradeId = pofreader.readString(0);
    // contextIdentifier = (Identifier)pofreader.readObject(1);
    }

    @Override
    public void writeExternal(PofWriter pofwriter) throws IOException {
    // TODO Auto-generated method stub
    pofwriter.writeString(0, tradeId);
    //pofwriter.writeObject(1, contextIdentifier);
    }

    public String getTradeId()
    {
    return tradeId;
    }
    /**
    *
    * returns hash code for this object
    */
    public int hashCode() {

    int result = 1;

    result = 31
    * result
    + ((tradeId == null) ? 0 : tradeId
    .hashCode());
    return result;
    }

    /**
    *
    */
    public boolean equals(Object obj) {
    if (this == obj)
    return true;
    if (obj == null)
    return false;
    if (getClass() != obj.getClass())
    return false;
    TradeCacheKey other = (TradeCacheKey) obj;

    if (tradeId == null) {
    if (other.tradeId != null)
    return false;
    } else if (!tradeId.equals(other.tradeId))
    return false;

    return true;
    }

    @Override
    public Object getAssociatedKey() {
    return contextIdentifier;
    }

    }

    I put the key to cache as
    tradeCache.put(new TradeCacheKey(trade.getTradeId(),contextIdentifier), “TEST”);
    System.out.println(” Entry inside cache is ” + tradeHistoryCache.get(new TradeCacheKey(trade.getTradeId())));

    Here the second line pri nts Entry inside cache is null.
    More interestingly if i get all the keys from cache and print values inside cache it gives me null
    Set<Entry> entrySet = tradeCache.entrySet();
    Iterator<Entry> itr = entrySet.iterator();
    while(itr.hasNext()){
    Entry entry = itr.next();
    TradeCacheKey key = (TradeCacheKey)entry.getKey();
    System.out.println(“Key is ” + key);
    System.out.println(“Get value using key ” + tradeCache.get(key));
    System.out.println(“Get value using new key ” + tradeCache.get(new TradeCacheKey(key.getTradeId())));

    }
    The output is
    Key is com.csfb.fid.gtb.tradecache.TradeCacheKey@28ccdc70
    Get value using key null
    Get value using new key null

    I dont know whether am i going wrong somewhere by not adding transient field to hashcode, equals, readexternal and writeexternal?

    Anil

    15 Dec 10 at 5:30 pm

  8. This is a pretty common problem. I too had written about something similar a while ago – http://javaforu.blogspot.com/2009/12/primary-key-object-under-appreciated.html

  9. One easy way to screw this up is to use java serialization or any serialisation that uses stream back references for reference equals objects. Because then your serialised form is dependent on whether your contents are interned. Eg a simple pair of integer (1,1) could end up having an inconsistent binary form if the 1 objects are reference equals. So, use EL or POF for keys, and no tricks.

    Rjw

    30 May 11 at 6:31 am

  10. RJW: this is absolutely correct! It doesn’t happen too often but I have seen it before. It can be very confusing if you don’t understand how Java serialization works internally.

    Patrick Peralta

    30 May 11 at 10:01 pm

  11. Very interesting post, how does this work in case of key affinity. As we need to de serialize the key to identify the association information.

    Ashish Garg

    10 Apr 12 at 10:30 am

  12. Great question! We do deserialize on the server to identify the association information. However, in 3.7.1 we added a feature to identify partition information in the binary itself so that .NET and C++ applications no longer require a corresponding Java key on the server side in order to use affinity. See http://docs.oracle.com/cd/E24290_01/coh.371/e22839/net_intobjects.htm under section “Deferring the Key Association Check” for details.

    Patrick Peralta

    10 Apr 12 at 11:37 am

  13. On the similar question related to affinity just wanted to clear one doubt. The key affinity is based on the hash of the parent key not whether the parent key is present in the cache at that time.

    Just to add an example let say Key B is associated with Key A. Now let say we had a parallel cache loader which first loads the Key B. So Key will be stored in the partition based on associated key details. Also when Key A is loaded it will fall in the same partition as that of its associated key B.

    Ashish Garg

    24 Apr 12 at 2:23 pm

  14. This is correct Ashish – the associated key does not need to be physically present in the cache in order for affinity to work for a given entry.

    Patrick Peralta

    24 Apr 12 at 3:47 pm

Leave a Reply