8000 fixed concurrency bug in date formatting and parsing · arangodb/spring-data@c55912d · GitHub
[go: up one dir, main page]

Skip to content

Commit c55912d

Browse files
committed
fixed concurrency bug in date formatting and parsing
1 parent 51dc9f1 commit c55912d

File tree

2 files changed

+222
-70
lines changed

2 files changed

+222
-70
lines changed

src/main/java/com/arangodb/springframework/core/convert/JavaTimeUtil.java

Lines changed: 69 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -20,83 +20,82 @@
2020

2121
package com.arangodb.springframework.core.convert;
2222

23-
import java.text.DateFormat;
2423
import java.text.ParseException;
25-
import java.text.SimpleDateFormat;
2624
import java.time.*;
2725
import java.time.format.DateTimeFormatter;
28-
import java.util.TimeZone;
26+
import java.util.Date;
27+
import java.time.format.DateTimeParseException;
2928

3029
/**
3130
* @author Mark Vollmary
32-
*
3331
*/
3432
public class JavaTimeUtil {
3533

36-
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
37-
38-
static {
39-
DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
40-
}
41-
42-
private JavaTimeUtil() {
43-
super();
44-
}
45-
46-
public static String format(final Instant source) {
47-
return DateTimeFormatter.ISO_INSTANT.format(source);
48-
}
49-
50-
public static String format(final LocalDate source) {
51-
return DateTimeFormatter.ISO_LOCAL_DATE.format(source);
52-
}
53-
54-
public static String format(final OffsetDateTime source) {
55-
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(source);
56-
}
57-
58-
public static String format(final LocalDateTime source) {
59-
return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(source);
60-
}
61-
62-
public static String format(final LocalTime source) {
63-
return DateTimeFormatter.ISO_LOCAL_TIME.format(source);
64-
}
65-
66-
public static String format(final ZonedDateTime source) {
67-
return DateTimeFormatter.ISO_ZONED_DATE_TIME.format(source);
68-
}
69-
70-
public static Instant parseInstant(final CharSequence source) {
71-
return Instant.parse(source);
72-
}
73-
74-
public static LocalDate parseLocalDate(final CharSequence source) {
75-
return LocalDate.parse(source);
76-
}
77-
78-
public static LocalDateTime parseLocalDateTime(final CharSequence source) {
79-
return LocalDateTime.parse(source);
80-
}
81-
82-
public static LocalTime parseLocalTime(final CharSequence source) {
83-
return LocalTime.parse(source);
84-
}
85-
86-
public static OffsetDateTime parseOffsetDateTime(final CharSequence source) {
87-
return OffsetDateTime.parse(source);
88-
}
89-
90-
public static ZonedDateTime parseZonedDateTime(final CharSequence source) {
91-
return ZonedDateTime.parse(source);
92-
}
93-
94-
public static java.util.Date parse(final String source) throws ParseException {
95-
return DATE_FORMAT.parse(source);
96-
}
97-
98-
public static String format(final java.util.Date date) {
99-
return DATE_FORMAT.format(date);
100-
}
101-
34+
private final static DateTimeFormatter DATE_FORMATTER = DateTimeFormatter
35+
.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
36+
.withZone(ZoneOffset.UTC);
37+
38+
private JavaTimeUtil() {
39+
super();
40+
}
41+
42+
public static String format(final Instant source) {
43+
return DateTimeFormatter.ISO_INSTANT.format(source);
44+
}
45+
46+
public static String format(final LocalDate source) {
47+
return DateTimeFormatter.ISO_LOCAL_DATE.format(source);
48+
}
49+
50+
public static String format(final OffsetDateTime source) {
51+
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(source);
52+
}
53+
54+
public static String format(final LocalDateTime source) {
55+
return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(source);
56+
}
57+
58+
public static String format(final LocalTime source) {
59+
return DateTimeFormatter.ISO_LOCAL_TIME.format(source);
60+
}
61+
62+
public static String format(final ZonedDateTime source) {
63+
return DateTimeFormatter.ISO_ZONED_DATE_TIME.format(source);
64+
}
65+
66+
public static Instant parseInstant(final CharSequence source) {
67+
return Instant.parse(source);
68+
}
69+
70+
public static LocalDate parseLocalDate(final CharSequence source) {
71+
return LocalDate.parse(source);
72+
}
73+
74+
public static LocalDateTime parseLocalDateTime(final CharSequence source) {
75+
return LocalDateTime.parse(source);
76+
}
77+
78+
public static LocalTime parseLocalTime(final CharSequence source) {
79+
return LocalTime.parse(source);
80+
}
81+
82+
public static OffsetDateTime parseOffsetDateTime(final CharSequence source) {
83+
return OffsetDateTime.parse(source);
84+
}
85+
86+
public static ZonedDateTime parseZonedDateTime(final CharSequence source) {
87+
return ZonedDateTime.parse(source);
88+
}
89+
90+
public static Date parse(final String source) throws ParseException {
91+
try {
92+
return new Date(parseZonedDateTime(source).toInstant().toEpochMilli());
93+
} catch (DateTimeParseException e) {
94+
throw new ParseException("Unparseable date: \"" + e.getParsedString() + "\"", e.getErrorIndex());
95+
}
96+
}
97+
98+
public static String format(final Date date) {
99+
return DATE_FORMATTER.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneOffset.UTC));
100+
}
102101
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package com.arangodb.springframework.core.convert;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.text.DateFormat;
6+
import java.text.ParseException;
7+
import java.text.SimpleDateFormat;
8+
import java.util.Date;
9+
import java.util.TimeZone;
10+
import java.util.concurrent.atomic.AtomicReference;
11+
import java.util.function.Consumer;
12+
import java.util.stream.IntStream;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
import static org.junit.jupiter.api.Assertions.assertThrows;
16+
17+
public class JavaTimeUtilTest {
18+
private static final DateFormat OLD_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
19+
20+
static {
21+
OLD_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
22+
}
23+
24+
private Date oldParse(final String source) throws ParseException {
25+
return OLD_DATE_FORMAT.parse(source);
26+
}
27+
28+
private static String oldFormat(final Date date) {
29+
return OLD_DATE_FORMAT.format(date);
30+
}
31+
32+
@Test
33+
void roundTrip() throws ParseException {
34+
var input = "2018-04-16T15:17:21.005Z";
35+
Date d = JavaTimeUtil.parse(input);
36+
String f = JavaTimeUtil.format(d);
37+
assertThat(f).isEqualTo(input);
38+
}
39+
40+
@Test
41+
void oldRoundTrip() throws ParseException {
42+
var input = "2018-04-16T15:17:21.005Z";
43+
Date d = oldParse(input);
44+
String f = oldFormat(d);
45+
assertThat(f).isEqualTo(input);
46+
}
47+
48+
@Test
49+
void parse() throws ParseException {
50+
var input = "2018-04-16T15:17:21.005Z";
51+
assertThat(JavaTimeUtil.parse(input)).isEqualTo(oldParse(input));
52+
}
53+
54+
@Test
55+
void oldParseException() {
56+
var input = "2018/04/16T15:17:21.005Z";
57+
ParseException e = assertThrows(ParseException.class, () -> oldParse(input));
58+
assertThat(e.getMessage()).contains(input);
59+
assertThat(e.getErrorOffset()).isEqualTo(4);
60+
}
61+
62+
@Test
63+
void parseException() {
64+
var input = "2018/04/16T15:17:21.005Z";
65+
ParseException e = assertThrows(ParseException.class, () -> JavaTimeUtil.parse(input));
66+
assertThat(e.getMessage()).contains(input);
67+
assertThat(e.getErrorOffset()).isEqualTo(4);
68+
}
69+
70+
@Test
71+
void format() {
72+
var input = new Date(1723642733350L);
73+
assertThat(JavaTimeUtil.format(input)).isEqualTo(oldFormat(input));
74+
}
75+
76+
@Test
77+
void concurrentParse() {
78+
testConcurrent(date -> JavaTimeUtil.parse(JavaTimeUtil.format(date)));
79+
}
80+
81+
@Test
82+
void oldConcurrentParse() {
83+
assertThrows(RuntimeException.class, () -> testConcurrent(date -> oldParse(JavaTimeUtil.format(date))));
84+
}
85+
86+
@Test
87+
void concurrentFormat() {
88+
testConcurrent(JavaTimeUtil::format);
89+
}
90+
91+
@Test
92+
void oldConcurrentFormat() {
93+
assertThrows(RuntimeException.class, () -> testConcurrent(JavaTimeUtilTest::oldFormat));
94+
}
95+
96+
private void testConcurrent(ThrowingConsumer<Date> fn) {
97+
AtomicReference<Throwable> e = new AtomicReference<>();
98+
Date[] dates;
99+
try {
100+
dates = new Date[]{
101+
JavaTimeUtil.parse("2018-04-16T15:17:21.005Z"),
102+
JavaTimeUtil.parse("2019-04-16T15:17:21.020Z")
103+
};
104+
} catch (ParseException ex) {
105+
throw new RuntimeException(ex);
106+
}
107+
108+
var threads = IntStream.range(0, 16)
109+
.mapToObj(i -> dates[i % dates.length])
110+
.map(date -> new Thread(() -> {
111+
for (int j = 0; j < 10_000; j++) {
112+
fn.accept(date);
113+
}
114+
}))
115+
.toList();
116+
117+
for (Thread t : threads) {
118+
t.setUncaughtExceptionHandler((th, ex) -> {
119+
e.set(ex);
120+
});
121+
t.start();
122+
}
123+
124+
for (Thread t : threads) {
125+
try {
126+
t.join();
127+
} catch (InterruptedException ex) {
128+
throw new RuntimeException(ex);
129+
}
130+
}
131+
132+
Throwable thrown = e.get();
133+
if (thrown != null) {
134+
throw new RuntimeException(thrown);
135+
}
136+
}
137+
138+
@FunctionalInterface
139+
private interface ThrowingConsumer<T> extends Consumer<T> {
140+
141+
@Override
142+
default void accept(final T elem) {
143+
try {
144+
acceptThrows(elem);
145+
} catch (final Exception e) {
146+
throw new RuntimeException(e);
147+
}
148+
}
149+
150+
void acceptThrows(T elem) throws Exception;
151+
}
152+
153+
}

0 commit comments

Comments
 (0)
0