4

Problem description

I have a java project with gradle dependency from org.javamoney:moneta:1.3.

Also I have two Kubernetes clusters. I deploy my java application using docker-container.

When I deploy my app in the first Kubernetes cluster everything is fine. But when I deploy my app (the same docker-container) in the second Kubernetes cluster following error appears:

javax.money.MonetaryException: No MonetaryAmountsSingletonSpi loaded.
    at javax.money.Monetary.lambda$getDefaultAmountFactory$13(Monetary.java:291)
    at java.base/java.util.Optional.orElseThrow(Optional.java:408)
    at javax.money.Monetary.getDefaultAmountFactory(Monetary.java:291)

It appears in the following code:

MonetaryAmount amount = javax.money.Monetary.getDefaultAmountFactory()
    .setCurrency("USD")
    .setNumber(1L)
    .create();

Software versions

  • Moneta: 1.3.
  • Gradle: 6.0.1.
  • Base docker-image: openjdk:11.0.7-jdk-slim.
  • Spring boot: 2.2.7.RELEASE.
  • Kubernetes (the same version on both clusters): Server Version: version.Info{Major:"1", Minor:"15", GitVersion:"v1.15.3", GitCommit:"2d3c76f9091b6bec110a5e63777c332469e0cba2", GitTreeState:"clean", BuildDate:"2019-08-19T11:05:50Z", GoVersion:"go1.12.9", Compiler:"gc", Platform:"linux/amd64"}.
  • Java: java -version openjdk version "11.0.7" 2020-04-14 OpenJDK Runtime Environment 18.9 (build 11.0.7+10) OpenJDK 64-Bit Server VM 18.9 (build 11.0.7+10, mixed mode).

What I have tried

Declare gradle-dependency differently

I found this question and it gave me an idea try to declare gradle-dependency in some different way. I have tried:

  • implementation 'org.javamoney:moneta:1.3'
  • compile group: 'org.javamoney', name: 'moneta', version: '1.3', ext: 'pom'
  • compile 'org.javamoney:moneta:1.3'
  • runtimeOnly 'org.javamoney:moneta:1.3'

Unfortunately, it did not give any positive results.

Copy-paste service loader configurations for Moneta

As mentioned in this comment I've tried to copy service loader configuration from Moneta to following project directory: src/main/resources/META-INF/services.

Unfortunately, it didn't help.

Init custom currency without spring

I've tried to do it just in the Main-class, but it didn't solve the problem.

Questions

  1. What is the root-cause of this problem?
  2. What is the proper solution to this problem?
Maksim Iakunin
  • 428
  • 1
  • 4
  • 21
  • did you also add money-api to your dependencies? https://mvnrepository.com/artifact/javax.money/money-api/1.0.3 – Michael Kreutz Apr 27 '20 at 19:19
  • @MichaelKreutz seems that is not needed. Cause `money-api` is a transitive dependency of `moneta`. See my gradle dependency graph here: https://gist.github.com/iakunin/ce96cca608f596f21f29508057061808 – Maksim Iakunin Apr 27 '20 at 19:30
  • I was unsure since I did not see it under compile dependencies in https://mvnrepository.com/artifact/org.javamoney/moneta/1.3. but it seems to come in with the parent pom.. – Michael Kreutz Apr 27 '20 at 19:47
  • Is there any guarantee that initialisation has happened before your use of the library, or could it be some kind of race condition? See: https://en.wikipedia.org/wiki/Java_memory_model on what Java guarantees for ordering independent of the underlying hardware that may be different for these clusters – JohannesB May 05 '20 at 21:20
  • What kind of packaging do you use? And in which environment do you deploy the app? Is it OSGI or something else? – Babl May 06 '20 at 14:40
  • @Babl it's a standalone Spring Boot application packaged in a jar using gradle. Did I understand you question in a right way? – Maksim Iakunin May 06 '20 at 15:44
  • @MaksimIakunin yes that's right, and where in the code are you registering a new Currency ? Is it in some component with `@PostConstruct` ? or Some `@Configuration` or elsewhere ? – Babl May 06 '20 at 15:58
  • @Babl Currency is registered in a class marked with `@Configuration` annotation. I've updated my question with exact code snippet. – Maksim Iakunin May 06 '20 at 16:27
  • @MaksimIakunin have you tried to manually put the Service loader configurations for Moneta into your project ? Like copy-paste them into your local META-INF/services, as seems somehow they are not loaded from the spring boot packaged jar. – Babl May 06 '20 at 16:43
  • @Babl just tried to copy-paste these Moneta service loader configurations to project `src/main/resources/META-INF/services` directory - it didn't help. Maybe I should put them to another directory? – Maksim Iakunin May 07 '20 at 19:52
  • Are 100% sure it is the same docker image used on both clusters? Can you verify the sha hash (using `kubectl describe pod your-pod-name`) of the images of th two pods in the two clusters are actually the same? – Victor Noël May 08 '20 at 12:54
  • @VictorNoël thanks for your comment. Yes, I'm 100% sure about that. I just verified sha hashes on both clusters and they are exactly the same. – Maksim Iakunin May 08 '20 at 15:55

2 Answers2

3

TL;DR

The problem was in concurrent moneta SPI initialization within Java 11.

Problem solution

The problem can be solved by extracting MonetaryAmountFactory to spring-bean and injecting it where needed:

@Bean
public MonetaryAmountFactory<?> money() {
    return Monetary.getDefaultAmountFactory();
}


@Component
@RequiredArgsConstructor
public static class Runner implements CommandLineRunner {

    private final MonetaryAmountFactory<?> amountFactory;

    @Override
    public void run(String... args) {
        var monetaryAmount = this.amountFactory
            .setCurrency("EUR")
            .setNumber(1)
            .create();

        System.out.println("monetaryAmount = " + monetaryAmount);
    }
}

instead of using this factory directly:

public static class Runner implements CommandLineRunner {

    @Override
    public void run(String... args) {
        var monetaryAmount = Monetary.getDefaultAmountFactory()
            .setCurrency("EUR")
            .setNumber(1)
            .create();

        System.out.println("monetaryAmount = " + monetaryAmount);
    }
}

Why problem occurs on Kubernetes-clusters?

I discovered that there were diferrent resource limit configuration on above-mentioned Kubernetes-clusters.

Cluster with exception:

Limits:
  cpu:     6
  memory:  20G
Requests:
  cpu:      3
  memory:   20G

Cluster without exception:

Limits:
  cpu:     2
  memory:  2G
Requests:
  cpu:      2
  memory:   128Mi

Seems that cluster with more resources gives more opportunity to concurrent moneta initialization happened.

Minimal reproducible example

The minimal reproducible example can be found in this github-repository.

It is worth mentioned that the bug is not reproduced on Java 8.

Maksim Iakunin
  • 428
  • 1
  • 4
  • 21
1

as a workaround you can create a service provider like

public class MyServiceLoader implements ServiceProvider {
/**
 * List of services loaded, per class.
 */
private final ConcurrentHashMap<Class<?>, List<Object>> servicesLoaded = new ConcurrentHashMap<>();
private static final int PRIORITY = 10;

/**
 * Returns a priority value of 10.
 *
 * @return 10, overriding the default provider.
 */
@Override
public int getPriority() {
    return PRIORITY;
}

/**
 * Loads and registers services.
 *
 * @param serviceType The service type.
 * @param <T>         the concrete type.
 * @return the items found, never {@code null}.
 */
@Override
public <T> List<T> getServices(final Class<T> serviceType) {
    @SuppressWarnings("unchecked")
    List<T> found = (List<T>) servicesLoaded.get(serviceType);
    if (found != null) {
        return found;
    }

    return loadServices(serviceType);
}

public static int compareServices(Object o1, Object o2) {
    int prio1 = 0;
    int prio2 = 0;
    Priority prio1Annot = o1.getClass().getAnnotation(Priority.class);
    if (prio1Annot != null) {
        prio1 = prio1Annot.value();
    }
    Priority prio2Annot = o2.getClass().getAnnotation(Priority.class);
    if (prio2Annot != null) {
        prio2 = prio2Annot.value();
    }
    if (prio1 < prio2) {
        return 1;
    }
    if (prio2 < prio1) {
        return -1;
    }
    return o2.getClass().getSimpleName().compareTo(o1.getClass().getSimpleName());
}

/**
 * Loads and registers services.
 *
 * @param serviceType The service type.
 * @param <T>         the concrete type.
 * @return the items found, never {@code null}.
 */
private <T> List<T> loadServices(final Class<T> serviceType) {
    List<T> services = new ArrayList<>();
    try {
        for (T t : ServiceLoader.load(serviceType, Monetary.class.getClassLoader())) {
            services.add(t);
        }
        services.sort(CbplMonetaServiceProvider::compareServices);
        @SuppressWarnings("unchecked") final List<T> previousServices = (List<T>) servicesLoaded.putIfAbsent(serviceType, (List<Object>) services);
        return Collections.unmodifiableList(previousServices != null ? previousServices : services);
    } catch (Exception e) {
        Logger.getLogger(CbplMonetaServiceProvider.class.getName()).log(Level.WARNING,
                "Error loading services of type " + serviceType, e);
        services.sort(CbplMonetaServiceProvider::compareServices);
        return services;
    }
}
}

and before using any money library class call

Bootstrap.init(new CbplMonetaServiceProvider());

this will fix the Currency error too.

the only changed line on the provider we added compared to the PriorityAwareServiceProvider is this line

for(T service:ServiceLoader.load(serviceType, Monetary.class.getClassLoader())){

we just specified the class loader so instead of Thread.getCurrentThread().getClassLoader() it is using the class loader we provide.

Utkan Ozyurek
  • 638
  • 7
  • 20