Search code examples
wildflyinfinispanjgroups

Invalid lambda deserialization with Infinispan Cache and computeIfAbsent


I’m toying with an basic infinispan cluster and I came across a puzzling error.

I’m basically implementing a shared map, holding just one Integer

Here is the code of my service

package sandbox.infinispan.test.service;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.inject.Named;

import org.infinispan.Cache;

@Named("useThisOne")
@ApplicationScoped
public class CounterService implements ICounterService {

    private static final String KEY = "key";

    @Inject
    private Cache<String, Integer> cache;

    @Override
    public void inc(final int amount) {
        this.cache.put(KEY, Integer.valueOf(this.get() + amount));
    }

    @Override
    public int get() {
        return this.cache.computeIfAbsent(KEY, k -> Integer.valueOf(0)).intValue();
    }
}

Cache is produced with the following:

package sandbox.infinispan.test.config;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;

import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfiguration;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;

@Dependent
class CacheProvider {

    @Produces
    @ApplicationScoped
    private EmbeddedCacheManager defaultClusteredCacheManager() {
        final GlobalConfiguration g = new GlobalConfigurationBuilder() //
                .clusteredDefault() //
                .transport() //
                .nodeName(this.getNodeName()) //
                .clusterName("infinispanTestCluster") //
                .build();
        final Configuration cfg = new ConfigurationBuilder() //
                .clustering() //
                .cacheMode(CacheMode.REPL_SYNC) ///
                .build();
        return new DefaultCacheManager(g, cfg);
    }
}

When there are at least two servers in the cluster, computeIfAbsent fails with

15:48:50,253 ERROR [org.infinispan.interceptors.impl.InvocationContextInterceptor] (jgroups-7,myhostname-14393) ISPN000136: Error executing command ComputeIfAbsentCommand, writing keys [key]: org.infinispan.remoting.RemoteException: ISPN000217: Received exception from otherhostname-44445, see cause for remote stack trace

which drills down to:

Caused by: java.lang.NoSuchMethodException: sandbox.infinispan.test.service.CounterService.$deserializeLambda$(java.lang.invoke.SerializedLambda)

and finally to:

Caused by: java.lang.IllegalArgumentException: Invalid lambda deserialization
        at sandbox.infinispan.test.service.CounterService.$deserializeLambda$(CounterService.java:10)
            ... 68 more
Caused by: an exception which occurred:
        in object of type java.lang.invoke.SerializedLambda

If I rewrite my pretty nice fashionable code to the ugly following, it works.

@Override
public int get() {
    Integer value = this.cache.get(KEY);
    if (value == null) {
        value = Integer.valueOf(0);
        this.cache.put(KEY, value);
    }
    return value.intValue();
}

How can I use the pretty computeIfAbsent way of doing things nowadays ?


Eclipse 2018-12, WildFly 14, java 10 on of the dev member of the cluster, CentOs 7, OpenJdk 10, WildFly 14 on the remote cluster member.

Thanks for your help


Solved (kinda)

Thanks to the help I received here, I transformed the lambda into an inner class :

static class OhWell implements Serializable {

    static Integer zero(final String t) {
        return Integer.valueOf(0);
    }
}

@Override
public int get() {
    return this.cache.computeIfAbsent(KEY, OhWell::zero).intValue();
}

It works now, but it’s lots less nice than the neat lambda. So I’ll stick to the old-fashioned way – unless someone can think of a better way to do it.


Further results:

The following static inner class with a static method works

static class StaticOhWell implements Serializable {

    static Integer apply(final String t) {
        return Integer.valueOf(0);
    }
}

@Override
public int get() {
    return this.cache.computeIfAbsent(KEY, StaticOhWell::apply).intValue();
}

The following non static inner class with a non static method fails :

class NotStaticOhWell implements SerializableFunction<String, Integer> {

    @Override
    public Integer apply(final String t) {
            return Integer.valueOf(0);
    }
}

@Override
public int get() {
    return this.cache.computeIfAbsent(KEY, new NotStaticOhWell()::apply).intValue();
}

It fails with this error message NotSerializableException: org.infinispan.cache.impl.EncoderCache:

13:41:29,221 ERROR [org.infinispan.interceptors.impl.InvocationContextInterceptor] (default task-1) ISPN000136: Error executing command ComputeIfAbsentCommand, writing keys [value]: org.infinispan.commons.marshall.NotSerializableException: org.infinispan.cache.impl.EncoderCache
    Caused by: an exception which occurred:
    in field sandbox.infinispan.test.service.CounterService.cache
    in object sandbox.infinispan.test.service.CounterService@4612a6c3
    in field sandbox.infinispan.test.service.CounterService$NotStaticOhWell.this$0
    in object sandbox.infinispan.test.service.CounterService$NotStaticOhWell@4effd362
    in field java.lang.invoke.SerializedLambda.capturedArgs
    in object java.lang.invoke.SerializedLambda@e62f08a
    in object sandbox.infinispan.test.service.CounterService$$Lambda$1195/1060417313@174a143b

Final words (?)

Using a “static lambda” (a static inner class implementing the SerializableFunction interface) worked too

static class StaticSerializableFunction implements SerializableFunction<String, Integer> {

    @Override
    public Integer apply(final String t) {
        return Integer.valueOf(0);
    }
}

@Override
public int get() {
    return this.cache.computeIfAbsent(KEY, new StaticSerializableFunction()::apply).intValue();
}


And the winner is…

Making the class actually serializable by “transienting” the Cache allows to simply use a method of this class. No need to create an inner class!

package sandbox.infinispan.test.service;

import java.io.Serializable;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.inject.Named;

import org.infinispan.Cache;

@Named("useThisOne")
@ApplicationScoped
public class CounterService implements ICounterService, Serializable {

    private static final String KEY = "value";

    @SuppressWarnings("cdi-ambiguous-dependency")
    @Inject
    private transient Cache<String, Integer> cache;

    @Override
    public void inc(final int amount) {
        this.cache.put(KEY, Integer.valueOf(this.get() + amount));
    }

    @Override
    public int get() {
        return this.cache.computeIfAbsent(KEY, this::zero).intValue();
    }

    private Integer zero(@SuppressWarnings("unused") final String unused) {
        return Integer.valueOf(0);
    }

    @Override
    public void reset() {
        this.cache.clear();
    }
}

Thanks all!


Solution

  • According to Unable to deserialize lambda the deserializer needs the actual code to be available. Are you sure that your application is already started on all other nodes in the cluster (the exact same version, including your lambda)?

    The computeIfAbsent() sends the lambda directly to the data (and therefore handles the operation using one RPC, instead of first fetching the value and then writing it as the 'ugly code'). In WF, your application lives in a different classloader (module) than Infinispan and that might cause an issue.

    Could you try to refactor your lambda into a class and see if you get similar problem? I am not that knowledgeable about WF, so there might be a mitigation for regular classes that is missing for lambdas.