committed by
GitHub
2 changed files with 130 additions and 2 deletions
@ -0,0 +1,127 @@ |
|||
/** |
|||
* 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.Calendar; |
|||
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; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
|
|||
@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<ListenableFuture<String>> 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<List<String>> future = Futures.allAsList(futures); |
|||
Futures.addCallback(future, new FutureCallback<List<String>>() { |
|||
@Override |
|||
public void onSuccess(List<String> 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); |
|||
} |
|||
} |
|||
|
|||
@Test |
|||
void testToISOStringThreadLocalStaticFormatter() throws ExecutionException, InterruptedException, TimeoutException { |
|||
executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)); |
|||
long ts = 1709217342987L; //Thu Feb 29 2024 14:35:42.987 GMT+0000
|
|||
int offset = Calendar.getInstance().get(Calendar.ZONE_OFFSET); // for example 3600000 for GMT + 1
|
|||
TbDate tbDate = new TbDate(ts - offset); |
|||
String datePrefix = "2024-02-29T14:35:42.987"; //without time zone
|
|||
assertThat(tbDate.toISOString()) |
|||
.as("format in main thread") |
|||
.startsWith(datePrefix); |
|||
assertThat(executor.submit(tbDate::toISOString).get(30, TimeUnit.SECONDS)) |
|||
.as("format in executor thread") |
|||
.startsWith(datePrefix); |
|||
assertThat(new TbDate(ts - offset).toISOString()) |
|||
.as("new instance format in main thread") |
|||
.startsWith(datePrefix); |
|||
assertThat(executor.submit(() -> new TbDate(ts - offset).toISOString()).get(30, TimeUnit.SECONDS)) |
|||
.as("new instance format in executor thread") |
|||
.startsWith(datePrefix); |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue