Неверная лямбда-десериализация с Infinispan Cache и calculateIfAbsent

Я экспериментировал с базовым кластером infinispan и обнаружил загадочную ошибку.

Я в основном реализую общую карту, содержащую только одно целое число.

Вот код моего сервиса

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();
    }
}

Кэш создается следующим образом:

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);
    }
}

Если в кластере есть как минимум два сервера, computeIfAbsent завершается ошибкой с

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

который сводится к:

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

и, наконец, к:

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

Если я перепишу свой довольно красивый модный код, сделав его уродливым, он сработает.

@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();
}

Как я могу использовать симпатичный метод computeIfAbsent в наши дни?


Eclipse 2018-12, WildFly 14, java 10 на члене-разработчике кластера, CentOs 7, OpenJdk 10, WildFly 14 на удаленном члене кластера.

Спасибо за вашу помощь


Решено (вроде)

Благодаря помощи, которую я получил здесь, я преобразовал лямбду во внутренний класс:

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();
}

Теперь это работает, но гораздо менее приятно, чем аккуратная лямбда. Так что я буду придерживаться старомодного способа, если только кто-нибудь не придумает лучший способ сделать это.


Дальнейшие результаты:

Следующий внутренний класс static с методом static работает

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();
}

Следующий нестатический внутренний класс с нестатическим методом завершается ошибкой:

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();
}

Это завершается ошибкой с этим сообщением об ошибке 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

Заключительные слова (?)

Использование «статической лямбды» (внутренний статический класс, реализующий интерфейс SerializableFunction) также работало.

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();
}


И победителем становится…

Делая класс действительно сериализуемым путем «перехода» Cache, можно просто использовать метод этого класса. Нет необходимости создавать внутренний класс!

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();
    }
}

Спасибо всем!


person Sxilderik    schedule 01.04.2019    source источник
comment
Существует ли класс CounterService на удаленном узле? Вы пытались изменить лямбду на анонимный внутренний класс?   -  person Dan Berindei    schedule 02.04.2019
comment
@DanBerindei спасибо, перемещение лямбды во внутренний класс (хотя и не анонимно) помогло.   -  person Sxilderik    schedule 02.04.2019
comment
Боюсь, вы только перенесли реализацию во вложенный класс, OhWell::zero все еще лямбда. Но поскольку он ссылается только на статический метод во вложенном классе, он ничего не фиксирует и определенно сериализуем.   -  person Dan Berindei    schedule 03.04.2019
comment
Перечитав описание, я почти уверен, что теперь Caused by: java.lang.NoSuchMethodException: sandbox.infinispan.test.service.CounterService.$deserializeLambda$(java.lang.invoke.SerializedLambda) означает, что класс CounterService, развернутый на удаленном узле, был скомпилирован до того, как вы добавили лямбду, и ваша исходная лямбда будет работать, если вы развернете один и тот же класс CounterService на всех узлах.   -  person Dan Berindei    schedule 03.04.2019
comment
@DanBerindei нет. Я убедился, что один и тот же код работает как в wildfly (flies?), так и в десериализации Invalid lambda.   -  person Sxilderik    schedule 03.04.2019
comment
Сериализуемый внутренний класс требует, чтобы окружающий класс также был сериализуемым.   -  person pxcv7r    schedule 05.01.2021
comment
Читая документы Cache.computeIfAbsent(), я понимаю, что реализация неанонимного класса, реализующего SerializableFunction, — это то, что разработчики Infinispan считают правильным.   -  person pxcv7r    schedule 05.01.2021


Ответы (1)


В соответствии с Невозможно десериализовать лямбда, десериализатору требуется, чтобы фактический код был доступен. Вы уверены, что ваше приложение уже запущено на всех остальных узлах кластера (точно такой же версии, включая вашу лямбду)?

computeIfAbsent() отправляет лямбду непосредственно в данные (и, следовательно, обрабатывает операцию, используя один RPC, вместо того, чтобы сначала получить значение, а затем записать его как «уродливый код»). В WF ваше приложение живет в другом загрузчике классов (модуле), чем Infinispan, и это может вызвать проблему.

Не могли бы вы попытаться реорганизовать свою лямбду в класс и посмотреть, не возникнет ли у вас аналогичная проблема? Я не так хорошо разбираюсь в WF, поэтому для обычных классов может быть смягчение, отсутствующее для лямбда-выражений.

person Radim Vansa    schedule 02.04.2019
comment
Спасибо. Я реорганизовал свою лямбду для использования внутреннего класса, реализовав SerializableFunction‹String, Integer›, и это сработало! Не знаю, как переход от аккуратной лямбда-выражения к громоздкому явному внутреннему классу может сделать мой код более приятным... Это работает, но я думаю, что буду придерживаться очевидного прежнего способа ведения дел. - person Sxilderik; 02.04.2019
comment
Я не говорю, что это лучше (очевидно, менее удобно) - просто хотел узнать, работает ли это... - person Radim Vansa; 03.04.2019
comment
Как вы можете видеть в моем отредактированном посте, реализация SerializableFunction‹String, Integer› на самом деле не работает. Вместо этого мне пришлось написать статический метод. Не могли бы вы это прокомментировать? - person Sxilderik; 03.04.2019
comment
Реализации интерфейса Serializable недостаточно, чтобы сделать класс действительно сериализуемым; если это внутренний класс, он содержит ссылку на внешний класс (CounterService), и он не сериализуем, поскольку имеет несериализуемое поле Cache. - person Radim Vansa; 04.04.2019
comment
Я не думаю, что вам нужно использовать внутренний класс StaticOhWell; статический метод в CounterService также должен работать. Как видите, это приводит к несвязанному (и понятному) исключению NotSerializableException. Однако я не совсем понимаю причины IllegalArgumentException. - person Radim Vansa; 04.04.2019
comment
Я думаю, что даже нестатический метод в CounterService тоже работает! - person Sxilderik; 05.04.2019