Skip to content

Commit b652d5f

Browse files
committed
Fix #384: change handling of SmileBufferRecycler to be thread-safe
1 parent a87e07a commit b652d5f

File tree

4 files changed

+93
-19
lines changed

4 files changed

+93
-19
lines changed

release-notes/CREDITS-2.x

+5
Original file line numberDiff line numberDiff line change
@@ -283,3 +283,8 @@ Kyle Silver (kyle-silver@github)
283283

284284
* Reported *379: (avro) `logback-test.xml` in wrong place (avro/src/main/resources)
285285
(2.15.2)
286+
287+
Simon Daudin (@simondaudin)
288+
289+
* Reported #384: `Smile` decoding issue with `NonBlockingByteArrayParser`, concurrency
290+
(2.15.3)

release-notes/VERSION-2.x

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ Active maintainers:
1414
=== Releases ===
1515
------------------------------------------------------------------------
1616

17+
2.15.3 (not yet released)
18+
19+
#384: `Smile` decoding issue with `NonBlockingByteArrayParser`, concurrency
20+
(reported by Simon D)
21+
1722
2.15.2 (30-May-2023)
1823

1924
#379: (avro) `logback-test.xml` in wrong place (avro/src/main/resources)
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.fasterxml.jackson.dataformat.smile;
22

3+
import java.util.concurrent.atomic.AtomicReference;
4+
35
/**
46
* Simple helper class used for implementing simple reuse system for Smile-specific
57
* buffers that are used.
@@ -12,40 +14,27 @@ public class SmileBufferRecycler<T>
1214

1315
public final static int DEFAULT_STRING_VALUE_BUFFER_LENGTH = 64;
1416

15-
protected T[] _seenNamesBuffer;
17+
protected AtomicReference<T[]> _seenNamesBuffer = new AtomicReference<>();
1618

17-
protected T[] _seenStringValuesBuffer;
19+
protected AtomicReference<T[]> _seenStringValuesBuffer = new AtomicReference<>();
1820

1921
public SmileBufferRecycler() { }
2022

2123
public T[] allocSeenNamesBuffer()
2224
{
23-
// 11-Feb-2011, tatu: Used to alloc here; but due to generics, can't easily any more
24-
T[] result = _seenNamesBuffer;
25-
if (result != null) {
26-
// let's ensure we don't retain it here, unless returned
27-
_seenNamesBuffer = null;
28-
// note: caller must have cleaned it up before returning
29-
}
30-
return result;
25+
return _seenNamesBuffer.getAndSet(null);
3126
}
3227

3328
public T[] allocSeenStringValuesBuffer()
3429
{
35-
// 11-Feb-2011, tatu: Used to alloc here; but due to generics, can't easily any more
36-
T[] result = _seenStringValuesBuffer;
37-
if (result != null) {
38-
_seenStringValuesBuffer = null;
39-
// note: caller must have cleaned it up before returning
40-
}
41-
return result;
30+
return _seenStringValuesBuffer.getAndSet(null);
4231
}
4332

4433
public void releaseSeenNamesBuffer(T[] buffer) {
45-
_seenNamesBuffer = buffer;
34+
_seenNamesBuffer.set(buffer);
4635
}
4736

4837
public void releaseSeenStringValuesBuffer(T[] buffer) {
49-
_seenStringValuesBuffer = buffer;
38+
_seenStringValuesBuffer.set(buffer);
5039
}
5140
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.fasterxml.jackson.dataformat.smile.async;
2+
3+
import java.io.IOException;
4+
import java.util.*;
5+
import java.util.concurrent.CompletableFuture;
6+
import java.util.concurrent.ExecutorService;
7+
import java.util.concurrent.Executors;
8+
9+
import com.fasterxml.jackson.core.*;
10+
import com.fasterxml.jackson.core.async.ByteArrayFeeder;
11+
import com.fasterxml.jackson.core.type.TypeReference;
12+
import com.fasterxml.jackson.databind.*;
13+
import com.fasterxml.jackson.databind.util.TokenBuffer;
14+
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
15+
16+
// for [dataformats-binary#384]
17+
public class ConcurrentAsyncTest extends AsyncTestBase
18+
{
19+
public void testConcurrentHandling() throws Exception
20+
{
21+
Map<String, Map<String, String>> tags = new HashMap<>();
22+
for (int i = 0; i < 10; i++) {
23+
Map<String, String> value = new HashMap<>();
24+
for (int j = 0; j < 10; j++) {
25+
value.put("key_" + j, "val" + j);
26+
}
27+
tags.put("elt_" + i, value);
28+
}
29+
30+
JsonFactory jsonFactory = new SmileFactory();
31+
ObjectMapper objectMapper = new ObjectMapper();
32+
ObjectWriter objectWriter = objectMapper.writer().with(jsonFactory);
33+
jsonFactory.setCodec(objectMapper);
34+
byte[] json = objectWriter.writeValueAsBytes(tags);
35+
TypeReference<Map<String, Map<String, String>>> typeReference = new TypeReference<Map<String, Map<String, String>>>() {
36+
};
37+
38+
ExecutorService executorService = Executors.newFixedThreadPool(10);
39+
List<CompletableFuture<?>> futures = new ArrayList<>();
40+
41+
// Exact count varies but this seems to be enough to produce the problem
42+
int count = 10_000;
43+
for (int i = 0; i < count; i++) {
44+
JsonParser parser = jsonFactory.createNonBlockingByteArrayParser();
45+
ByteArrayFeeder inputFeeder = (ByteArrayFeeder) parser.getNonBlockingInputFeeder();
46+
futures.add(CompletableFuture.supplyAsync(() -> {
47+
try {
48+
inputFeeder.feedInput(json, 0, json.length);
49+
@SuppressWarnings("resource")
50+
TokenBuffer tokenBuffer = new TokenBuffer(parser);
51+
while (true) {
52+
JsonToken token = parser.nextToken();
53+
if (token == JsonToken.NOT_AVAILABLE || token == null) {
54+
break;
55+
}
56+
57+
tokenBuffer.copyCurrentEvent(parser);
58+
}
59+
return tokenBuffer.asParser(jsonFactory.getCodec()).readValueAs(typeReference);
60+
} catch (IOException e) {
61+
throw new RuntimeException(e);
62+
} finally {
63+
try {
64+
inputFeeder.endOfInput();
65+
parser.close();
66+
} catch (IOException e) {
67+
throw new RuntimeException(e);
68+
}
69+
}
70+
}, executorService));
71+
}
72+
73+
CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])).get();
74+
}
75+
}

0 commit comments

Comments
 (0)