diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java new file mode 100644 index 0000000000..11d65e0ba7 --- /dev/null +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java @@ -0,0 +1,102 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.script.api.tbel; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +@Slf4j +class TbDateTest { + + ListeningExecutorService executor; + + @AfterEach + void tearDown() { + if (executor != null) { + executor.shutdownNow(); + } + } + + /** + * Note: This test simulated the high concurrency calls. + * But it not always fails before as the concurrency issue happens far away from the test subject, inside the SimpleDateFormat class. + * Few calls later after latch open, the concurrency issue is not well reproduced. + * Depends on environment some failure may happen each 2 or 100 runs. + * To be highly confident run this test in a ForkMode=method, repeat 100 times. This will provide about 99 failures per 100 runs. + * The value of this test is *never* to fail when isoDateFormat.format(this) is properly synchronized + * If this test fails time-to-time -- it is a sign that isoDateFormat.format() called concurrently and have to be fixed (synchronized) + * The expected exception example: + * Caused by: java.lang.ArrayIndexOutOfBoundsException: Index 14 out of bounds for length 13 + * at java.base/sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:457) + * at java.base/java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2394) + * at java.base/java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2309) + * at java.base/java.util.Calendar.setTimeInMillis(Calendar.java:1834) + * at java.base/java.util.Calendar.setTime(Calendar.java:1800) + * at java.base/java.text.SimpleDateFormat.format(SimpleDateFormat.java:974) + */ + @Test + void testToISOStringConcurrently() throws ExecutionException, InterruptedException, TimeoutException { + int threads = 5; + executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(threads)); + for (int j = 0; j < 1000; j++) { + final int iteration = j; + CountDownLatch readyLatch = new CountDownLatch(threads); + CountDownLatch latch = new CountDownLatch(1); + long now = 1709217342000L; + List> futures = new ArrayList<>(threads); + for (int i = 0; i < threads; i++) { + long ts = now + TimeUnit.DAYS.toMillis(i * 366) + TimeUnit.MINUTES.toMillis(iteration) + TimeUnit.SECONDS.toMillis(iteration) + iteration; + TbDate tbDate = new TbDate(ts); + futures.add(executor.submit(() -> { + readyLatch.countDown(); + if (!latch.await(30, TimeUnit.SECONDS)) { + throw new RuntimeException("await timeout"); + } + return tbDate.toISOString(); + })); + } + ListenableFuture> future = Futures.allAsList(futures); + Futures.addCallback(future, new FutureCallback>() { + @Override + public void onSuccess(List result) { + + } + + @Override + public void onFailure(Throwable t) { + log.error("Failure happens on iteration {}", iteration); + } + }, MoreExecutors.directExecutor()); + readyLatch.await(30, TimeUnit.SECONDS); + latch.countDown(); + future.get(30, TimeUnit.SECONDS); + } + } +} \ No newline at end of file