0

I have a problem with Apache Ignite Serialization/Deserialization related to the order in which fields are deserialized. I need to put an instance of "B" as follow in the Ignite cache:

public class A {
    private final String name;

    public A(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}


public class B extends A {

    private Map<B, String> mapOfB;

    public B(String name) {
        super(name);

        mapOfB = new HashMap<>();
    }

    public void addB(B newB, String someString) {
        mapOfB.put(newB, someString);
    }

    public Map<B, String> getMap() {
        return mapOfB;
    }

    @Override
    public boolean equals(Object obj) {
        if( obj != null && obj instanceof B) {
            if(this.getName() == null && ((B) obj).getName() == null && this == obj) {
                return true;
            } else if(this.getName().equals(((B) obj).getName())) {
                return true;
            }
        }
        return false;
    }

    @Override
    public int hashCode() {
        return this.getName()==null? System.identityHashCode(this):this.getName().hashCode();
    }
}

The if I run the following code:

    public static void main(String[] args) {
        // write your code here
        B b1 = new B("first");
        b1.addB(b1, "some first string");
        B b2 = new B("second");
        b1.addB(b2, "some second string");

        // init Ignite configuration
        // force java.util.Hashtable to be binary serialized, 
        // it prevents infinite recursion and  other problems 
        // occurring with the Optimized Serializer
        IgniteConfiguration cfg = new IgniteConfiguration();
        BinaryConfiguration binConf = new BinaryConfiguration();
        Collection<String> binClassNames = new LinkedList<>();
        binClassNames.add("java.util.Hashtable");
        binConf.setClassNames(binClassNames);
        cfg.setBinaryConfiguration(binConf);
        Ignition.start(cfg);

        // put b1 in cache
        IgniteCache cache = Ignition.ignite().getOrCreateCache("MyCache");
        cache.put(b1.hashCode(), b1);

        //get b1 from cache
        B b1FromCache= (B) cache.get(b1.hashCode());

        // print map values
        System.out.println("b1 map value: " + b1.getMap().get(b1));
        System.out.println("b1 from cache map value: " + b1FromCache.getMap().get(b1));
    }

Output is

b1 map value: some first string

b1 from cache map value: null

The problem is that fields from the child are deserialized before fields from the parent, so when Ignite deserializes B, it first creates an empty B object (with null "name" and "mapOfB"), then it tries to deserialize mapOfB. It creates the Hashtable and then deserialize each object it contains to fill it.

For b2 in the example above there is no problem as no reference to b2 exists yet when it is deserialized, so a new b2 object is created, b2 fields are populated (including the "name" field), then it is added to the Hashmap with correct hash.

For b1 however deserialization started, so the object already exists in Ignit's map of deserialized objects, but with null name (deserialization is in process for b1), and with a hashCode computed with this null name. The Hashtable puts b1 with the hashCode computed at that time, so when we try to find b1 with non null name in the map at the end it cannot be found.

I cannot change A and B classes and the way these objects are created (how the Hashmap is filled,...) so it has to be solved by changing the serialization. Is there a simple way to do that?

Remark: actual code is way more complicated than that, with may classes between actual B and the Hashmap.

Nicolas
  • 23
  • 1
  • 7
  • What could work is something like `b1.addB(new B("first"), "some first string")` but this is super flaky and only works because of how `hasCode` and `equals` are implemented... `B` should not be used as keys of a map since it is not immutable. – Alexandre Dupriez Nov 24 '17 at 14:46
  • Thanks. I agree it is ugly but I can't change this code. I also agree your solution could work but can't do that either as I don't even have control about how the hashMap is filled. I'll edit the text to add this, thanks again. – Nicolas Nov 24 '17 at 15:04

1 Answers1

1

You are correct about the reason of such behaviour. mapOfB field is deserialized before the name field, and hashCode() depends on that name. And fields of B are changed after you put it into the map as a key, so it's hashCode changes.

I'd recommend you to change your data model, but since you can't, here is another option... OptimizedMarshaller doesn't seem to have a problem with Maps, because it uses a simple Java serialization. But you won't be able to use BinaryObject abstraction and a few other features with it. Here is how you can enable OptimizedMarshaller:

OptimizedMarshaller marshaller = new OptimizedMarshaller();
cfg.setMarshaller(marshaller);

If you store values, that don't implement Serializable interface, then you may need to configure it appropriately:

marshaller.setRequireSerializable(false);

But note, that disabling requireSerializable flag may affect serialization performance in a negative way.

Denis
  • 3,573
  • 8
  • 13
  • Thanks for your answer. You don't actually need to force the OptimizedMarshaller; as Hashmap implements Serializable, it is serialized by default by the OptimizedMarshaller with all contained elements. This however leads to infinite recursion, as the HashMap contains b1 which contains the Hashmap... and the OptimizedMarshaller doesn't prevent infinite recursion, this is why it can't be used here. To prevent this we have to force binary serialization for the Hashmap (among many other java.util classes for the full application). – Nicolas Nov 27 '17 at 09:38
  • You will only need to set requireSerializable to false, if class B doesn't implement Serializable, otherwise it works fine. I didn't encounter any problems with infinite recursion, with configured OptimizedMarshaller for your example. And it also works fine, if I remove BinaryConfiguration completely. What version of Ignite do you use? Can you provide a reproducer, that causes the problem and an exception? It's probably better to do as a separate SO question. – Denis Nov 27 '17 at 10:12
  • You are right, your solution works. Thank you very much for this! I use Ignite 2.2.0. – Nicolas Nov 27 '17 at 10:51