javacachinginfinispan

How is it possible that a key exists in cache but cannot be retrieved through cache.get(key)?


Context: There is an Infinispan (13.0.10) cache that uses a custom data type Quad<String, Long, Type, ValidityPeriod> as key. As far as I can tell all hashCode() and equals(Object o)-methods are either auto-generated or implemented correctly. Also the cache is used in Local Mode.

Question: When I try to call cache.get(Object key) with a Quad that I know the cache contains, I get null. cache.containsKey(Object key) also returns false.

How is this possible?

Clarification: I said that I know the cache contains the Quad key because I have done the following using the Eclipse IDE debugger:

cache.entrySet().stream().filter(e -> e.getKey().hashCode == Quad.of(a, b, c, d).hashCode()).collect(toList());
// this returns a non-empty list with my expected return
cache.entrySet().stream().filter(e -> e.getKey().equals(Quad.of(a, b, c, d))).collect(toList());
// this returns a non-empty list with my expected return

Per Radim Vansa's suggestion I have also tried the following:

cache.entrySet().stream().collect(Collectors.toMap(Entry::getKey, Entry::getValue, (o1,o2) -> o1, ConcurrentHashMap::new)).get(Quad.of(a, b, c, d));
// this returns my expected return

Further Context (if needed): I am working on an older project where I am supposed to update from Infinispan Version 10 to 13. I have successfully done so and also integrated ProtoStream API instead of using the old JBossMarshaller. This version change is important because the retrieval stopped working after the update.

There are multiple Caches in use, some are indexed using custom data types such as Pair<K, V>, Triple<L, M, R> and Quad<A, B, C, D> which are all generic. I have written some ProtoAdpaters for all of them and they work just fine.

I now have come across a problem, where there is an AdvancedCache that uses a Quad like this as key:

Quad<String, Long, Type, ValidityPeriod>

Quad overrides equals(Object o) and hashCode as follows:

public class Quad<A, B, C, D> {

    // other code omitted for brevity

    public boolean equals(final Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj instanceof Quad<?, ?, ?, ?>) {
            final Quad<?, ?, ?, ?> other = (Quad<?, ?, ?, ?>) obj;
            return Objects.equals(getFirst(), other.getFirst())
                && Objects.equals(getSecond(), other.getSecond())
                && Objects.equals(getThird(), other.getThird())
                && Objects.equals(getFourth(), other.getFourth());
        }
        return false;
    }

    public int hashCode() {
        return Objects.hashCode(getFirst()) 
                ^ Objects.hashCode(getSecond()) 
                ^ Objects.hashCode(getThird()) 
                ^ Objects.hashCode(getFourth());
    }
}

For reference Type is structured along the lines of:

public class Type implements Serializable {
    private int fieldA;
    private int fieldB;
    private String fieldC;

    // hashCode and equals are auto-generated
    // constructor, getters and setters omitted for brevity
}

ValidityPeriod is something like this:

public class ValidityPeriod implements Serializable {
    private LocalDate validFrom;
    private LocalDate invalidFrom;

    // hashCode and equals are auto-generated
    // constructor, getters and setters omitted for brevity
}

The marshaller for LocalDate uses the following adapter:

@ProtoAdapter(LocalDate.class)
public class LocalDateAdapter {
    
    @ProtoFactory
    LocalDate create(int year, short month, short day) {
        return LocalDate.of(year, month, month);
    }
    
    @ProtoField(number = 1, required = true)
    int getYear(LocalDate localDate) {
        return localDate.getYear();
    }
    
    @ProtoField(number = 2, required = true)
    short getMonth(LocalDate localDate) {
        return (short) localDate.getMonth().getValue();
    }
    
    @ProtoField(number = 3, required = true)
    short getDay(LocalDate localDate) {
        return (short) localDate.getDayOfMonth();
    }
}

I have tried to use the debugger to understand the internal workings of Infinispan but I don't seem to be able to pin down a concrete line producing this error. As far as I know it has something to do with CacheImpl.get(Object, long, InvocationContext).

Update: Ok, per Pruivo's suggestion I have tried to create a reprex. The strange thing however is that I have tried to copy every process that happens before the object is retrieved from the cache, but in the reprex I created it works. Funny thing however is the following I tried: I created two methods in ValidityPeriod that do the following (almost like Infinispan Transformers):

public String toFormString() {
    return String.format("%s§%s", validFrom, invalidFrom);
}
    
public static ValidityPeriod fromFormString(String form) {
    String[] split = form.split("§");
    return from(LocalDate.parse(split[0]),LocalDate.parse(split[1]));
}

Then I've changed the Quad to Quad<String, Long, Type, String> while constructing the key for the cache with strings from these methods instead of the ValidityPeriod itself. Strangely enough, this fixes my original problem. However, as this is a dirty fix, I am not content with keeping this solution permanent. There has to be something wrong with ValidityPeriod is my guess.

I still am confused as to why the cache returns something different than it contains, so I will still leave my original question open.


Solution

  • After a careful deduction following Pruivo's suggestions, I could pin down a cause for this bug. It is the ProtoAdapter that is written for LocalDate in which an incorrect LocalDate-object is created upon deserialization.

    Concrete fix: The concrete fix for this case is the following:

    @ProtoAdapter(LocalDate.class)
    public class LocalDateAdapter {
        
        @ProtoFactory
        LocalDate create(int year, short month, short day) {
            return LocalDate.of(year, month, day); // <= here was the error
        }
        
        @ProtoField(number = 1, required = true)
        int getYear(LocalDate localDate) {
            return localDate.getYear();
        }
        
        @ProtoField(number = 2, required = true)
        short getMonth(LocalDate localDate) {
            return (short) localDate.getMonth().getValue();
        }
        
        @ProtoField(number = 3, required = true)
        short getDay(LocalDate localDate) {
            return (short) localDate.getDayOfMonth();
        }
    }
    

    Explanation: As Pruivo stated, Infinispan uses hashCode() to determine a key's owner/segment. In the background Infinispan uses a ConcurrentHashMap for storing keys and values. This map contains serialized objects if the cache has the respective configuration parameter set.

    Now to the problem. The ConcurrentHashMap contains the following method that is called to retrieve an object:

    public V get(Object key) {
       Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
       int h = spread(key.hashCode());
       if ((tab = table) != null && (n = tab.length) > 0 &&
          (e = tabAt(tab, (n - 1) & h)) != null) { // <= line 5
             // this code here is not reached
             if ((eh = e.hash) == h) {
                    if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                        return e.val;
                }
                else if (eh < 0)
                    return (p = e.find(h, key)) != null ? p.val : null;
                while ((e = e.next) != null) {
                    if (e.hash == h &&
                        ((ek = e.key) == key || (ek != null && key.equals(ek))))
                        return e.val;
             }
          }
       }
       return null;
    }
    

    As you can see in line 5, there is a check for a method called tabAt(). This verifies that the object reference at that adress of the map is not null. If it is null then no further actions are taken and null will be returned. This includes that no comparison using the objects' hashes or equals takes place. You can see this in the lines that follow tabAt(). This would explain why cache.entrySet().stream()... could retrieve an object an get() couldn't.

    Now my educated guess why the object's reference isn't found and therefor is null: I guess that the original key is that of the correctly serialized object but after deserialization this key becomes corrupt because it changes value through the use of the faulty marshaller. As modification of a key in a Map can produce unexpected behaviour, this would explain the odd behaviour. (Source for unexpected behaviour of Map when keys are modified: https://stackoverflow.com/a/21601013/14945497)

    Suggestion/Remark: A final remark I would like to make is that this error can be quite inconspicuous and lead to other problems down the line because wrong objects are created upon deserialization. This creates a lot of confusion with strange behaviour and a lot of detective work needs to be done to get rid of it. With that in mind, my suggestion would be to double-triple-check your definitions for all marshallers.