jsr354-ri icon indicating copy to clipboard operation
jsr354-ri copied to clipboard

Under concurrency, Moneta formatting fails.

Open guilhermeblanco opened this issue 5 years ago • 3 comments

Bringing the discussion from Gitter to here, as I was asked to implement a test case without frameworks, just raw moneta. Below is my pom.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.javamoney</groupId>
            <artifactId>moneta</artifactId>
            <version>1.4.2</version>
            <type>pom</type>
        </dependency>
    </dependencies>

    <build>
        <finalName>service</finalName>

        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>

                <configuration>
                    <release>11</release>
                </configuration>
            </plugin>

            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>

                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>

                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <mainClass>com.example.test.Application</mainClass>
                        </manifest>
                    </archive>

                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

And my class:

package com.example.test;

import org.javamoney.moneta.Money;

import javax.money.MonetaryException;
import javax.money.format.MonetaryAmountFormat;
import javax.money.format.MonetaryFormats;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;

public class Application {
    private static final MonetaryAmountFormat MONEY_FORMATTER = MonetaryFormats.getAmountFormat(Locale.ENGLISH);

    public static void main(final String[] args) throws InterruptedException {
        final ExecutorService pool = Executors.newWorkStealingPool(100);
        final List<Callable<Boolean>> taskList = new ArrayList<>();

        for (int i = 0; i < 500000; i++) {
            final Integer startBidAmount = ThreadLocalRandom.current().nextInt(2, 70) * 1000;
            final Money startBidMoney = Money.of(startBidAmount, "CAD");
            final String money = MONEY_FORMATTER.format(startBidMoney);

            taskList.add(() -> {
                try {
                    Money.parse(money, MONEY_FORMATTER);
                } catch (MonetaryException e) {
                    System.out.println("Parsing money: " + money);
                    System.out.println(e.getMessage());
                }

                return true;
            });
        }

        pool.invokeAll(taskList);
        pool.shutdown();
    }
}

Code can be executed running:

sdk use java 11.0.7-amzn # Also reproducible on 11.0.7.hs-adpt
mvn clean install
java -jar target/service-jar-with-dependencies.jar

All reported variety of exceptions are now being reported. Below is an example of exceptions generated:

Parsing money: CAD28,000.00
Could not parse CurrencyUnit. Unknown currency code:
Aug. 18, 2020 10:42:55 A.M. org.javamoney.moneta.spi.format.DefaultMonetaryAmountFormat parse
INFO: Failed to parse positive pattern, trying negative for: CAD45,000.00
java.lang.NumberFormatException: For input string: "45.0045E05"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.base/java.lang.Long.parseLong(Long.java:692)
	at java.base/java.lang.Long.parseLong(Long.java:817)
	at java.base/java.text.DigitList.getLong(DigitList.java:195)
	at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2121)
	at org.javamoney.moneta.spi.format.AmountNumberToken.parseToken(AmountNumberToken.java:184)
	at org.javamoney.moneta.spi.format.AmountNumberToken.parse(AmountNumberToken.java:155)
	at org.javamoney.moneta.spi.format.DefaultMonetaryAmountFormat.parse(DefaultMonetaryAmountFormat.java:174)
	at org.javamoney.moneta.Money.parse(Money.java:883)
	at com.eblock.simulcast.test.Application.lambda$main$0(Application.java:33)
	at java.base/java.util.concurrent.ForkJoinTask$AdaptedCallable.exec(ForkJoinTask.java:1448)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
	at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
	at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
	at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
	at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)

Aug. 18, 2020 10:42:55 A.M. org.javamoney.moneta.spi.format.DefaultMonetaryAmountFormat parse
INFO: Failed to parse positive pattern, trying negative for: CAD46,000.00
java.lang.NumberFormatException: For input string: "4060.0"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.base/java.lang.Long.parseLong(Long.java:692)
	at java.base/java.lang.Long.parseLong(Long.java:817)
	at java.base/java.text.DigitList.getLong(DigitList.java:195)
	at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2121)
	at org.javamoney.moneta.spi.format.AmountNumberToken.parseToken(AmountNumberToken.java:184)
	at org.javamoney.moneta.spi.format.AmountNumberToken.parse(AmountNumberToken.java:155)
	at org.javamoney.moneta.spi.format.DefaultMonetaryAmountFormat.parse(DefaultMonetaryAmountFormat.java:174)
	at org.javamoney.moneta.Money.parse(Money.java:883)
	at com.eblock.simulcast.test.Application.lambda$main$0(Application.java:33)
	at java.base/java.util.concurrent.ForkJoinTask$AdaptedCallable.exec(ForkJoinTask.java:1448)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
	at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
	at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
	at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
	at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)

Parsing money: CAD15,000.00
Could not parse CurrencyUnit. Unknown currency code:
Parsing money: CAD45,000.00
Could not parse CurrencyUnit. Unknown currency code:
Parsing money: CAD27,000.00
Could not parse CurrencyUnit. Unknown currency code:

Aug. 18, 2020 10:42:55 A.M. org.javamoney.moneta.spi.format.DefaultMonetaryAmountFormat parse
INFO: Failed to parse positive pattern, trying negative for: CAD25,000.00
java.lang.NumberFormatException: multiple points
	at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.base/java.lang.Double.parseDouble(Double.java:543)
	at java.base/java.text.DigitList.getDouble(DigitList.java:169)
	at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2126)
	at org.javamoney.moneta.spi.format.AmountNumberToken.parseToken(AmountNumberToken.java:184)
	at org.javamoney.moneta.spi.format.AmountNumberToken.parse(AmountNumberToken.java:155)
	at org.javamoney.moneta.spi.format.DefaultMonetaryAmountFormat.parse(DefaultMonetaryAmountFormat.java:174)
	at org.javamoney.moneta.Money.parse(Money.java:883)
	at com.eblock.simulcast.test.Application.lambda$main$0(Application.java:33)
	at java.base/java.util.concurrent.ForkJoinTask$AdaptedCallable.exec(ForkJoinTask.java:1448)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
	at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
	at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
	at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
	at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)

Aug. 18, 2020 10:42:55 A.M. org.javamoney.moneta.spi.format.DefaultMonetaryAmountFormat parse
INFO: Failed to parse positive pattern, trying negative for: CAD38,000.00
java.lang.NumberFormatException: For input string: ".380380E05"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.base/java.lang.Long.parseLong(Long.java:678)
	at java.base/java.lang.Long.parseLong(Long.java:817)
	at java.base/java.text.DigitList.getLong(DigitList.java:195)
	at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2121)
	at org.javamoney.moneta.spi.format.AmountNumberToken.parseToken(AmountNumberToken.java:184)
	at org.javamoney.moneta.spi.format.AmountNumberToken.parse(AmountNumberToken.java:155)
	at org.javamoney.moneta.spi.format.DefaultMonetaryAmountFormat.parse(DefaultMonetaryAmountFormat.java:174)
	at org.javamoney.moneta.Money.parse(Money.java:883)
	at com.eblock.simulcast.test.Application.lambda$main$0(Application.java:33)
	at java.base/java.util.concurrent.ForkJoinTask$AdaptedCallable.exec(ForkJoinTask.java:1448)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
	at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
	at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
	at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
	at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)

Aug. 18, 2020 10:42:55 A.M. org.javamoney.moneta.spi.format.DefaultMonetaryAmountFormat parse
INFO: Failed to parse positive pattern, trying negative for: CAD61,000.00
java.lang.NumberFormatException: For input string: ""
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.base/java.lang.Long.parseLong(Long.java:702)
	at java.base/java.lang.Long.parseLong(Long.java:817)
	at java.base/java.text.DigitList.getLong(DigitList.java:195)
	at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2121)
	at org.javamoney.moneta.spi.format.AmountNumberToken.parseToken(AmountNumberToken.java:184)
	at org.javamoney.moneta.spi.format.AmountNumberToken.parse(AmountNumberToken.java:155)
	at org.javamoney.moneta.spi.format.DefaultMonetaryAmountFormat.parse(DefaultMonetaryAmountFormat.java:174)
	at org.javamoney.moneta.Money.parse(Money.java:883)
	at com.eblock.simulcast.test.Application.lambda$main$0(Application.java:33)
	at java.base/java.util.concurrent.ForkJoinTask$AdaptedCallable.exec(ForkJoinTask.java:1448)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
	at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
	at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
	at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
	at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)

guilhermeblanco avatar Aug 18 '20 14:08 guilhermeblanco

I'm not yet sure if it's only due to concurrency, but if the same works in a single thread it might.

keilw avatar Aug 18 '20 14:08 keilw

I'm not yet sure if it's only due to concurrency, but if the same works in a single thread it might.

I've commented out the Executor related portion and left only the loop executing the commands. Everything works perfectly fine.

guilhermeblanco avatar Aug 18 '20 15:08 guilhermeblanco

At least the number parts use NumberFormat which as java.text.Format in general are not thread-safe. Approaches like http://blog.vityuk.com/2011/03/java-formatters-best-practices.html might help, but it remains to be seen, if we can do this with a simple update release like 1.4.3 or 1.4.4 of Moneta or it could require a new JSR (which is also planned, but probably not started before early 2021)

keilw avatar Aug 18 '20 23:08 keilw

Is this still valid? The MonetaryAmountFormat docs do state that

Instances of this class are not required to be thread-safe

so changing this would require an update to the JSR, right?

kewne avatar Apr 13 '24 15:04 kewne

Not sure, if there was ever a PR for this, but the JavaDoc says it is not required to be thread-safe, it could still be thread-safe, that's optional. Closing it for now, if someone needs to revisit it, please open a new ticket.

keilw avatar Apr 15 '24 06:04 keilw