Skip to content

Commit 856d701

Browse files
committed
Merge pull request #5 from maxmind/greg/thread-pool-fix
Fix resource leak when using a thread pool
2 parents 261a0a1 + 862c224 commit 856d701

File tree

5 files changed

+60
-65
lines changed

5 files changed

+60
-65
lines changed

src/main/java/com/maxmind/db/ThreadBuffer.java renamed to src/main/java/com/maxmind/db/BufferHolder.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212

1313
import com.maxmind.db.Reader.FileMode;
1414

15-
final class ThreadBuffer extends ThreadLocal<ByteBuffer> implements Closeable {
15+
final class BufferHolder implements Closeable {
1616
// DO NOT PASS THESE OUTSIDE THIS CLASS. Doing so will remove thread
1717
// safety.
1818
private final ByteBuffer buffer;
1919
private final RandomAccessFile raf;
2020
private final FileChannel fc;
2121

22-
ThreadBuffer(File database, FileMode mode) throws IOException {
22+
BufferHolder(File database, FileMode mode) throws IOException {
2323
this.raf = new RandomAccessFile(database, "r");
2424
this.fc = this.raf.getChannel();
2525
if (mode == FileMode.MEMORY) {
@@ -41,7 +41,7 @@ final class ThreadBuffer extends ThreadLocal<ByteBuffer> implements Closeable {
4141
* @throws NullPointerException
4242
* if you provide a NULL InputStream
4343
*/
44-
ThreadBuffer(InputStream stream) throws IOException {
44+
BufferHolder(InputStream stream) throws IOException {
4545
if (null == stream) {
4646
throw new NullPointerException("Unable to use a NULL InputStream");
4747
}
@@ -57,14 +57,17 @@ final class ThreadBuffer extends ThreadLocal<ByteBuffer> implements Closeable {
5757
}
5858

5959
// This is just to ease unit testing
60-
ThreadBuffer(ByteBuffer buffer) {
60+
BufferHolder(ByteBuffer buffer) {
6161
this.buffer = buffer;
6262
this.raf = null;
6363
this.fc = null;
6464
}
6565

66-
@Override
67-
protected synchronized ByteBuffer initialValue() {
66+
/*
67+
* Returns a duplicate of the underlying ByteBuffer. The returned ByteBuffer
68+
* should not be shared between threads.
69+
*/
70+
synchronized ByteBuffer get() {
6871
return this.buffer.duplicate();
6972
}
7073

src/main/java/com/maxmind/db/Decoder.java

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
import com.fasterxml.jackson.databind.node.ObjectNode;
1919
import com.fasterxml.jackson.databind.node.TextNode;
2020

21+
/*
22+
* Decoder for MaxMind DB data.
23+
*
24+
* This class CANNOT be shared between threads
25+
*/
2126
final class Decoder {
2227
// XXX - This is only for unit testings. We should possibly make a
2328
// constructor to set this
@@ -26,7 +31,7 @@ final class Decoder {
2631

2732
private final ObjectMapper objectMapper;
2833

29-
private final ThreadBuffer threadBuffer;
34+
private final ByteBuffer buffer;
3035

3136
static enum Type {
3237
EXTENDED, POINTER, UTF8_STRING, DOUBLE, BYTES, UINT16, UINT32, MAP, INT32, UINT64, UINT128, ARRAY, CONTAINER, END_MARKER, BOOLEAN, FLOAT;
@@ -73,22 +78,21 @@ void setOffset(int offset) {
7378

7479
}
7580

76-
Decoder(ThreadBuffer threadBuffer, long pointerBase) {
81+
Decoder(ByteBuffer buffer, long pointerBase) {
7782
this.pointerBase = pointerBase;
78-
this.threadBuffer = threadBuffer;
83+
this.buffer = buffer;
7984
this.objectMapper = new ObjectMapper();
8085
}
8186

8287
Result decode(int offset) throws IOException {
83-
ByteBuffer buffer = this.threadBuffer.get();
84-
if (offset >= buffer.capacity()) {
88+
if (offset >= this.buffer.capacity()) {
8589
throw new InvalidDatabaseException(
8690
"The MaxMind DB file's data section contains bad data: "
8791
+ "pointer larger than the database.");
8892
}
8993

90-
buffer.position(offset);
91-
int ctrlByte = 0xFF & buffer.get();
94+
this.buffer.position(offset);
95+
int ctrlByte = 0xFF & this.buffer.get();
9296
offset++;
9397

9498
Type type = Type.fromControlByte(ctrlByte);
@@ -109,7 +113,7 @@ Result decode(int offset) throws IOException {
109113
}
110114

111115
if (type.equals(Type.EXTENDED)) {
112-
int nextByte = buffer.get();
116+
int nextByte = this.buffer.get();
113117

114118
int typeNum = nextByte + 7;
115119

@@ -188,7 +192,7 @@ private Result decodePointer(int ctrlByte, int offset) {
188192
}
189193

190194
private String decodeString(int size) {
191-
ByteBuffer buffer = this.threadBuffer.get().slice();
195+
ByteBuffer buffer = this.buffer.slice();
192196
buffer.limit(size);
193197
return Charset.forName("UTF-8").decode(buffer).toString();
194198
}
@@ -202,10 +206,9 @@ private IntNode decodeInt32(int size) {
202206
}
203207

204208
private long decodeLong(int size) {
205-
ByteBuffer buffer = this.threadBuffer.get();
206209
long integer = 0;
207210
for (int i = 0; i < size; i++) {
208-
integer = (integer << 8) | (buffer.get() & 0xFF);
211+
integer = (integer << 8) | (this.buffer.get() & 0xFF);
209212
}
210213
return integer;
211214
}
@@ -219,8 +222,7 @@ private int decodeInteger(int size) {
219222
}
220223

221224
private int decodeInteger(int base, int size) {
222-
ByteBuffer buffer = this.threadBuffer.get();
223-
return Decoder.decodeInteger(buffer, base, size);
225+
return Decoder.decodeInteger(this.buffer, base, size);
224226
}
225227

226228
static int decodeInteger(ByteBuffer buffer, int base, int size) {
@@ -242,7 +244,7 @@ private DoubleNode decodeDouble(int size) throws InvalidDatabaseException {
242244
"The MaxMind DB file's data section contains bad data: "
243245
+ "invalid size of double.");
244246
}
245-
return new DoubleNode(this.threadBuffer.get().getDouble());
247+
return new DoubleNode(this.buffer.getDouble());
246248
}
247249

248250
private FloatNode decodeFloat(int size) throws InvalidDatabaseException {
@@ -251,7 +253,7 @@ private FloatNode decodeFloat(int size) throws InvalidDatabaseException {
251253
"The MaxMind DB file's data section contains bad data: "
252254
+ "invalid size of float.");
253255
}
254-
return new FloatNode(this.threadBuffer.get().getFloat());
256+
return new FloatNode(this.buffer.getFloat());
255257
}
256258

257259
private static BooleanNode decodeBoolean(int size)
@@ -317,7 +319,7 @@ private int[] sizeFromCtrlByte(int ctrlByte, int offset) {
317319
}
318320

319321
private byte[] getByteArray(int length) {
320-
return Decoder.getByteArray(this.threadBuffer.get(), length);
322+
return Decoder.getByteArray(this.buffer, length);
321323
}
322324

323325
private static byte[] getByteArray(ByteBuffer buffer, int length) {

src/main/java/com/maxmind/db/Reader.java

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@ public final class Reader implements Closeable {
1919
(byte) 0xCD, (byte) 0xEF, 'M', 'a', 'x', 'M', 'i', 'n', 'd', '.',
2020
'c', 'o', 'm' };
2121

22-
private int ipV4Start;
23-
private final Decoder decoder;
22+
private final int ipV4Start;
2423
private final Metadata metadata;
25-
private final ThreadBuffer threadBuffer;
24+
private final BufferHolder bufferHolder;
2625

2726
/**
2827
* The file mode to use when opening a MaxMind DB.
@@ -63,7 +62,7 @@ public Reader(File database) throws IOException {
6362
* if there is an error reading from the Stream.
6463
*/
6564
public Reader(InputStream source) throws IOException {
66-
this(new ThreadBuffer(source), "<InputStream>");
65+
this(new BufferHolder(source), "<InputStream>");
6766
}
6867

6968
/**
@@ -78,17 +77,19 @@ public Reader(InputStream source) throws IOException {
7877
* if there is an error opening or reading from the file.
7978
*/
8079
public Reader(File database, FileMode fileMode) throws IOException {
81-
this(new ThreadBuffer(database, fileMode), database.getName());
80+
this(new BufferHolder(database, fileMode), database.getName());
8281
}
8382

84-
private Reader(ThreadBuffer buffer, String name) throws IOException {
85-
this.threadBuffer = buffer;
86-
int start = this.findMetadataStart(name);
83+
private Reader(BufferHolder bufferHolder, String name) throws IOException {
84+
this.bufferHolder = bufferHolder;
8785

88-
Decoder metadataDecoder = new Decoder(this.threadBuffer, start);
86+
ByteBuffer buffer = this.bufferHolder.get();
87+
int start = this.findMetadataStart(buffer, name);
88+
89+
Decoder metadataDecoder = new Decoder(buffer, start);
8990
this.metadata = new Metadata(metadataDecoder.decode(start).getNode());
90-
this.decoder = new Decoder(this.threadBuffer,
91-
this.metadata.searchTreeSize + DATA_SECTION_SEPARATOR_SIZE);
91+
92+
this.ipV4Start = this.findIpV4StartNode(buffer);
9293
}
9394

9495
/**
@@ -101,27 +102,28 @@ private Reader(ThreadBuffer buffer, String name) throws IOException {
101102
* if a file I/O error occurs.
102103
*/
103104
public JsonNode get(InetAddress ipAddress) throws IOException {
104-
int pointer = this.findAddressInTree(ipAddress);
105+
ByteBuffer buffer = this.bufferHolder.get();
106+
int pointer = this.findAddressInTree(buffer, ipAddress);
105107
if (pointer == 0) {
106108
return null;
107109
}
108-
return this.resolveDataPointer(pointer);
110+
return this.resolveDataPointer(buffer, pointer);
109111
}
110112

111-
private int findAddressInTree(InetAddress address)
113+
private int findAddressInTree(ByteBuffer buffer, InetAddress address)
112114
throws InvalidDatabaseException {
113115
byte[] rawAddress = address.getAddress();
114116

115117
int bitLength = rawAddress.length * 8;
116-
int record = this.startNode(bitLength);
118+
int record = this.startNode(buffer, bitLength);
117119

118120
for (int i = 0; i < bitLength; i++) {
119121
if (record >= this.metadata.nodeCount) {
120122
break;
121123
}
122124
int b = 0xFF & rawAddress[i / 8];
123125
int bit = 1 & (b >> 7 - (i % 8));
124-
record = this.readNode(record, bit);
126+
record = this.readNode(buffer, record, bit);
125127
}
126128
if (record == this.metadata.nodeCount) {
127129
// record is empty
@@ -133,38 +135,31 @@ record = this.readNode(record, bit);
133135
throw new InvalidDatabaseException("Something bad happened");
134136
}
135137

136-
private int startNode(int bitLength) throws InvalidDatabaseException {
138+
private int startNode(ByteBuffer buffer, int bitLength) throws InvalidDatabaseException {
137139
// Check if we are looking up an IPv4 address in an IPv6 tree. If this
138140
// is the case, we can skip over the first 96 nodes.
139141
if (this.metadata.ipVersion == 6 && bitLength == 32) {
140-
return this.ipV4StartNode();
142+
return this.ipV4Start;
141143
}
142144
// The first node of the tree is always node 0, at the beginning of the
143145
// value
144146
return 0;
145147
}
146148

147-
private int ipV4StartNode() throws InvalidDatabaseException {
148-
// This is a defensive check. There is no reason to call this when you
149-
// have an IPv4 tree.
149+
private int findIpV4StartNode(ByteBuffer buffer) throws InvalidDatabaseException {
150150
if (this.metadata.ipVersion == 4) {
151151
return 0;
152152
}
153153

154-
if (this.ipV4Start != 0) {
155-
return this.ipV4Start;
156-
}
157154
int node = 0;
158155
for (int i = 0; i < 96 && node < this.metadata.nodeCount; i++) {
159-
node = this.readNode(node, 0);
156+
node = this.readNode(buffer, node, 0);
160157
}
161-
this.ipV4Start = node;
162158
return node;
163159
}
164160

165-
private int readNode(int nodeNumber, int index)
161+
private int readNode(ByteBuffer buffer, int nodeNumber, int index)
166162
throws InvalidDatabaseException {
167-
ByteBuffer buffer = this.threadBuffer.get();
168163
int baseOffset = nodeNumber * this.metadata.nodeByteSize;
169164

170165
switch (this.metadata.recordSize) {
@@ -190,19 +185,21 @@ private int readNode(int nodeNumber, int index)
190185
}
191186
}
192187

193-
private JsonNode resolveDataPointer(int pointer) throws IOException {
188+
private JsonNode resolveDataPointer(ByteBuffer buffer, int pointer) throws IOException {
194189
int resolved = (pointer - this.metadata.nodeCount)
195190
+ this.metadata.searchTreeSize;
196191

197-
if (resolved >= this.threadBuffer.get().capacity()) {
192+
if (resolved >= buffer.capacity()) {
198193
throw new InvalidDatabaseException(
199194
"The MaxMind DB file's search tree is corrupt: "
200195
+ "contains pointer larger than the database.");
201196
}
202197

203198
// We only want the data from the decoder, not the offset where it was
204199
// found.
205-
return this.decoder.decode(resolved).getNode();
200+
Decoder decoder = new Decoder(buffer,
201+
this.metadata.searchTreeSize + DATA_SECTION_SEPARATOR_SIZE);
202+
return decoder.decode(resolved).getNode();
206203
}
207204

208205
/*
@@ -213,9 +210,8 @@ private JsonNode resolveDataPointer(int pointer) throws IOException {
213210
* are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
214211
* an issue, but I suspect it won't be.
215212
*/
216-
private int findMetadataStart(String databaseName)
213+
private int findMetadataStart(ByteBuffer buffer, String databaseName)
217214
throws InvalidDatabaseException {
218-
ByteBuffer buffer = this.threadBuffer.get();
219215
int fileSize = buffer.capacity();
220216

221217
FILE: for (int i = 0; i < fileSize - METADATA_START_MARKER.length + 1; i++) {
@@ -245,6 +241,6 @@ Metadata getMetadata() {
245241
*/
246242
@Override
247243
public void close() throws IOException {
248-
this.threadBuffer.close();
244+
this.bufferHolder.close();
249245
}
250246
}

src/test/java/com/maxmind/db/DecoderTest.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@
2222
import com.fasterxml.jackson.databind.node.ArrayNode;
2323
import com.fasterxml.jackson.databind.node.FloatNode;
2424
import com.fasterxml.jackson.databind.node.ObjectNode;
25-
import com.maxmind.db.Decoder;
26-
import com.maxmind.db.InvalidDatabaseException;
27-
import com.maxmind.db.ThreadBuffer;
2825

2926
@SuppressWarnings({ "boxing", "static-method" })
3027
public class DecoderTest {
@@ -426,7 +423,7 @@ static <T> void testTypeDecoding(Decoder.Type type, Map<T, byte[]> tests)
426423
MappedByteBuffer mmap = fc.map(MapMode.READ_ONLY, 0, fc.size());
427424
try {
428425

429-
Decoder decoder = new Decoder(new ThreadBuffer(mmap), 0);
426+
Decoder decoder = new Decoder(mmap, 0);
430427
decoder.POINTER_TEST_HACK = true;
431428

432429
// XXX - this could be streamlined

src/test/java/com/maxmind/db/PointerTest.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@
1010

1111
import com.fasterxml.jackson.databind.ObjectMapper;
1212
import com.fasterxml.jackson.databind.node.ObjectNode;
13-
import com.maxmind.db.Decoder;
14-
import com.maxmind.db.InvalidDatabaseException;
15-
import com.maxmind.db.ThreadBuffer;
1613
import com.maxmind.db.Reader.FileMode;
1714

1815
public class PointerTest {
@@ -22,9 +19,9 @@ public void testWithPointers() throws InvalidDatabaseException,
2219
IOException, URISyntaxException {
2320
File file = new File(PointerTest.class.getResource(
2421
"/maxmind-db/test-data/maps-with-pointers.raw").toURI());
25-
ThreadBuffer ptf = new ThreadBuffer(file, FileMode.MEMORY);
22+
BufferHolder ptf = new BufferHolder(file, FileMode.MEMORY);
2623
try {
27-
Decoder decoder = new Decoder(ptf, 0);
24+
Decoder decoder = new Decoder(ptf.get(), 0);
2825

2926
ObjectMapper om = new ObjectMapper();
3027

0 commit comments

Comments
 (0)