Skip to content

Commit 93fdc58

Browse files
committed
Made the buffer thread local, which should solve most of our thread safety issues.
1 parent 01c4b08 commit 93fdc58

9 files changed

Lines changed: 187 additions & 163 deletions

File tree

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

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import com.fasterxml.jackson.databind.node.ObjectNode;
1818
import com.fasterxml.jackson.databind.node.TextNode;
1919

20-
public class Decoder {
20+
public final class Decoder {
2121

2222
private final boolean DEBUG;
2323
// XXX - This is only for unit testings. We should possibly make a
@@ -27,7 +27,27 @@ public class Decoder {
2727

2828
private final ObjectMapper objectMapper;
2929

30-
private final ByteBuffer mmap;
30+
private final ThreadBuffer threadBuffer;
31+
32+
static enum Type {
33+
EXTENDED, POINTER, UTF8_STRING, DOUBLE, BYTES, UINT16, UINT32, MAP, INT32, UINT64, UINT128, ARRAY, CONTAINER, END_MARKER, BOOLEAN;
34+
35+
public static Type get(int i) {
36+
// XXX - Type.values() might be expensive. Consider caching it.
37+
return Type.values()[i];
38+
}
39+
40+
private static Type get(byte b) {
41+
// bytes are signed, but we want to treat them as unsigned here
42+
// XXX - Type.values() might be expensive. Consider caching it.
43+
return Type.get(b & 0xFF);
44+
}
45+
46+
public static Type fromControlByte(int b) {
47+
// The type is encoded in the first 3 bits of the byte.
48+
return Type.get((byte) ((0xFF & b) >>> 5));
49+
}
50+
}
3151

3252
class Result {
3353
private final JsonNode node;
@@ -52,21 +72,23 @@ void setOffset(long offset) {
5272

5373
}
5474

55-
public Decoder(ByteBuffer mmap, long pointerBase) {
75+
public Decoder(ThreadBuffer threadBuffer, long pointerBase) {
5676
this.pointerBase = pointerBase;
57-
this.mmap = mmap;
77+
this.threadBuffer = threadBuffer;
5878
this.objectMapper = new ObjectMapper();
5979
this.DEBUG = System.getenv().get("MAXMIND_DB_DECODER_DEBUG") != null;
6080
}
6181

6282
public Result decode(long offset) throws MaxMindDbException, IOException {
63-
this.mmap.position((int) offset);
83+
ByteBuffer buffer = this.threadBuffer.get();
84+
85+
buffer.position((int) offset);
6486

6587
if (this.DEBUG) {
6688
Log.debug("Offset", String.valueOf(offset));
6789
}
6890

69-
int ctrlByte = 0xFF & this.mmap.get();
91+
int ctrlByte = 0xFF & buffer.get();
7092

7193
offset++;
7294

@@ -95,7 +117,7 @@ public Result decode(long offset) throws MaxMindDbException, IOException {
95117
}
96118

97119
if (type.equals(Type.EXTENDED)) {
98-
int nextByte = this.mmap.get();
120+
int nextByte = buffer.get();
99121

100122
if (this.DEBUG) {
101123
Log.debug("Next byte", nextByte);
@@ -210,7 +232,7 @@ private Result decodePointer(int ctrlByte, long offset) {
210232
}
211233

212234
private String decodeString(int size) {
213-
ByteBuffer buffer = this.mmap.slice();
235+
ByteBuffer buffer = this.threadBuffer.get().slice();
214236
buffer.limit(size);
215237
return Charset.forName("UTF-8").decode(buffer).toString();
216238
}
@@ -228,9 +250,10 @@ private int decodeInteger(int size) {
228250
}
229251

230252
private int decodeInteger(int base, int size) {
253+
ByteBuffer buffer = this.threadBuffer.get();
231254
int integer = base;
232255
for (int i = 0; i < size; i++) {
233-
integer = (integer << 8) | (this.mmap.get() & 0xFF);
256+
integer = (integer << 8) | (buffer.get() & 0xFF);
234257
}
235258
return integer;
236259
}
@@ -244,9 +267,10 @@ long decodeLong(int size) {
244267
}
245268

246269
long decodeLong(long base, int size) {
270+
ByteBuffer buffer = this.threadBuffer.get();
247271
long longInt = base;
248272
for (int i = 0; i < size; i++) {
249-
longInt = (longInt << 8) | (this.mmap.get() & 0xFF);
273+
longInt = (longInt << 8) | (buffer.get() & 0xFF);
250274
}
251275
return longInt;
252276
}
@@ -325,8 +349,7 @@ private Result decodeMap(long size, long offset) throws MaxMindDbException,
325349

326350
}
327351

328-
private long[] sizeFromCtrlByte(int ctrlByte, long offset)
329-
throws IOException {
352+
private long[] sizeFromCtrlByte(int ctrlByte, long offset) {
330353
int size = ctrlByte & 0x1f;
331354

332355
if (size < 29) {
@@ -351,6 +374,28 @@ private long[] sizeFromCtrlByte(int ctrlByte, long offset)
351374
}
352375

353376
private byte[] getByteArray(int length) {
354-
return Util.getByteArray(this.mmap, length);
377+
return Decoder.getByteArray(this.threadBuffer.get(), length);
378+
}
379+
380+
static int decodeInteger(byte[] bytes) {
381+
int i = 0;
382+
for (byte b : bytes) {
383+
i = (i << 8) | (b & 0xFF);
384+
}
385+
return i;
386+
}
387+
388+
static long decodeLong(byte[] bytes) {
389+
long i = 0;
390+
for (byte b : bytes) {
391+
i = (i << 8) | (b & 0xFF);
392+
}
393+
return i;
394+
}
395+
396+
static byte[] getByteArray(ByteBuffer buffer, int length) {
397+
byte[] bytes = new byte[length];
398+
buffer.get(bytes);
399+
return bytes;
355400
}
356401
}

src/main/java/com/maxmind/maxminddb/Log.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import java.net.InetAddress;
44
import java.nio.ByteBuffer;
55

6-
class Log {
6+
final class Log {
77

88
static void debug(String string, ByteBuffer buffer) {
99
debug(string, buffer.array());

src/main/java/com/maxmind/maxminddb/Metadata.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import com.fasterxml.jackson.databind.JsonNode;
66

77
// XXX - if we make this public, add getters
8-
class Metadata {
8+
final class Metadata {
99
final int binaryFormatMajorVersion;
1010
final int binaryFormatMinorVersion;
1111
private final BigInteger buildEpoch;

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

Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,13 @@
22

33
import java.io.File;
44
import java.io.IOException;
5-
import java.io.RandomAccessFile;
65
import java.net.InetAddress;
76
import java.nio.ByteBuffer;
8-
import java.nio.MappedByteBuffer;
9-
import java.nio.channels.FileChannel;
10-
import java.nio.channels.FileChannel.MapMode;
117
import java.util.Arrays;
128

139
import com.fasterxml.jackson.databind.JsonNode;
1410

15-
public class Reader {
11+
public final class Reader {
1612
private static final int DATA_SECTION_SEPARATOR_SIZE = 16;
1713
private static final byte[] METADATA_START_MARKER = { (byte) 0xAB,
1814
(byte) 0xCD, (byte) 0xEF, 'M', 'a', 'x', 'M', 'i', 'n', 'd', '.',
@@ -21,15 +17,23 @@ public class Reader {
2117
private final boolean DEBUG;
2218
private final Decoder decoder;
2319
private final Metadata metadata;
24-
private final FileChannel fc;
25-
private final RandomAccessFile raf;
26-
private final MappedByteBuffer mmap;
20+
private final ThreadBuffer threadBuffer;
21+
22+
public enum FileMode {
23+
MEMORY_MAPPED, IN_MEMORY
24+
}
2725

2826
public Reader(File database) throws MaxMindDbException, IOException {
27+
this(database, FileMode.MEMORY_MAPPED);
28+
}
29+
30+
// XXX - loading the file into memory doesn't really provide any performance
31+
// gains on my machine. Consider whether it is even worth providing the
32+
// option.
33+
public Reader(File database, FileMode mode) throws MaxMindDbException,
34+
IOException {
2935
this.DEBUG = System.getenv().get("MAXMIND_DB_READER_DEBUG") != null;
30-
this.raf = new RandomAccessFile(database, "r");
31-
this.fc = this.raf.getChannel();
32-
this.mmap = this.fc.map(MapMode.READ_ONLY, 0, this.fc.size());
36+
this.threadBuffer = new ThreadBuffer(database, mode);
3337

3438
/*
3539
* We need to make sure that whatever chunk we read will have the
@@ -54,12 +58,12 @@ public Reader(File database) throws MaxMindDbException, IOException {
5458
+ "). Is this a valid MaxMind DB file?");
5559
}
5660

57-
Decoder metadataDecoder = new Decoder(this.mmap, 0);
61+
Decoder metadataDecoder = new Decoder(this.threadBuffer, 0);
5862

5963
this.metadata = new Metadata(metadataDecoder.decode(start).getNode());
6064

61-
this.decoder = new Decoder(this.mmap, this.metadata.searchTreeSize
62-
+ DATA_SECTION_SEPARATOR_SIZE);
65+
this.decoder = new Decoder(this.threadBuffer,
66+
this.metadata.searchTreeSize + DATA_SECTION_SEPARATOR_SIZE);
6367

6468
if (this.DEBUG) {
6569
Log.debug(this.metadata.toString());
@@ -75,12 +79,11 @@ public JsonNode get(InetAddress address) throws MaxMindDbException,
7579
return null;
7680
}
7781

78-
this.mmap.position((int) pointer);
82+
this.threadBuffer.get().position((int) pointer);
7983
return this.resolveDataPointer(pointer);
8084
}
8185

82-
long findAddressInTree(InetAddress address) throws MaxMindDbException,
83-
IOException {
86+
long findAddressInTree(InetAddress address) throws MaxMindDbException {
8487
byte[] rawAddress = address.getAddress();
8588

8689
// XXX sort of wasteful
@@ -141,35 +144,34 @@ long record = nodes[bit];
141144
throw new MaxMindDbException("Something bad happened");
142145
}
143146

144-
private long[] readNode(long nodeNumber) throws IOException,
145-
MaxMindDbException {
146-
this.mmap.position((int) nodeNumber * this.metadata.nodeByteSize);
147+
private long[] readNode(long nodeNumber) throws MaxMindDbException {
148+
ByteBuffer buffer = this.threadBuffer.get();
149+
buffer.position((int) nodeNumber * this.metadata.nodeByteSize);
147150

148-
byte[] buffer = Util
149-
.getByteArray(this.mmap, this.metadata.nodeByteSize);
151+
byte[] bytes = Decoder.getByteArray(buffer, this.metadata.nodeByteSize);
150152

151153
if (this.DEBUG) {
152-
Log.debug("Node bytes", buffer);
154+
Log.debug("Node bytes", bytes);
153155
}
154-
return this.splitNodeIntoRecords(buffer);
156+
return this.splitNodeIntoRecords(bytes);
155157
}
156158

157159
private long[] splitNodeIntoRecords(byte[] bytes) throws MaxMindDbException {
158160
long[] nodes = new long[2];
159161
switch (this.metadata.recordSize) {
160162
case 24:
161-
nodes[0] = Util.decodeLong(Arrays.copyOfRange(bytes, 0, 3));
162-
nodes[1] = Util.decodeLong(Arrays.copyOfRange(bytes, 3, 6));
163+
nodes[0] = Decoder.decodeLong(Arrays.copyOfRange(bytes, 0, 3));
164+
nodes[1] = Decoder.decodeLong(Arrays.copyOfRange(bytes, 3, 6));
163165
return nodes;
164166
case 28:
165-
nodes[0] = Util.decodeLong(Arrays.copyOfRange(bytes, 0, 3));
166-
nodes[1] = Util.decodeLong(Arrays.copyOfRange(bytes, 4, 7));
167+
nodes[0] = Decoder.decodeLong(Arrays.copyOfRange(bytes, 0, 3));
168+
nodes[1] = Decoder.decodeLong(Arrays.copyOfRange(bytes, 4, 7));
167169
nodes[0] = ((0xF0 & bytes[3]) << 20) | nodes[0];
168170
nodes[1] = ((0x0F & bytes[3]) << 24) | nodes[1];
169171
return nodes;
170172
case 32:
171-
nodes[0] = Util.decodeLong(Arrays.copyOfRange(bytes, 0, 4));
172-
nodes[1] = Util.decodeLong(Arrays.copyOfRange(bytes, 4, 8));
173+
nodes[0] = Decoder.decodeLong(Arrays.copyOfRange(bytes, 0, 4));
174+
nodes[1] = Decoder.decodeLong(Arrays.copyOfRange(bytes, 4, 8));
173175
return nodes;
174176
default:
175177
throw new MaxMindDbException("Unknown record size: "
@@ -203,15 +205,15 @@ private JsonNode resolveDataPointer(long pointer)
203205
* are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
204206
* an issue, but I suspect it won't be.
205207
*/
206-
private long findMetadataStart() throws IOException {
207-
long fileSize = this.fc.size();
208+
private long findMetadataStart() {
209+
ByteBuffer buffer = this.threadBuffer.get();
210+
int fileSize = buffer.capacity();
208211

209-
FILE: for (long i = 0; i < fileSize - METADATA_START_MARKER.length + 1; i++) {
212+
FILE: for (int i = 0; i < fileSize - METADATA_START_MARKER.length + 1; i++) {
210213
for (int j = 0; j < METADATA_START_MARKER.length; j++) {
211-
ByteBuffer b = ByteBuffer.wrap(new byte[1]);
212-
this.fc.read(b, fileSize - i - j - 1);
213-
if (b.get(0) != METADATA_START_MARKER[METADATA_START_MARKER.length
214-
- j - 1]) {
214+
byte b = buffer.get(fileSize - i - j - 1);
215+
if (b != METADATA_START_MARKER[METADATA_START_MARKER.length - j
216+
- 1]) {
215217
continue FILE;
216218
}
217219
}
@@ -220,16 +222,11 @@ private long findMetadataStart() throws IOException {
220222
return -1;
221223
}
222224

223-
public Metadata getMetadata() {
225+
Metadata getMetadata() {
224226
return this.metadata;
225227
}
226228

227229
public void close() throws IOException {
228-
if (this.fc != null) {
229-
this.fc.close();
230-
}
231-
if (this.raf != null) {
232-
this.raf.close();
233-
}
230+
this.threadBuffer.close();
234231
}
235232
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.maxmind.maxminddb;
2+
3+
import java.io.File;
4+
import java.io.IOException;
5+
import java.io.RandomAccessFile;
6+
import java.nio.ByteBuffer;
7+
import java.nio.channels.FileChannel;
8+
import java.nio.channels.FileChannel.MapMode;
9+
10+
import com.maxmind.maxminddb.Reader.FileMode;
11+
12+
final class ThreadBuffer extends ThreadLocal<ByteBuffer> {
13+
// XXX - DO NOT PASS THIS OUTSIDE THIS CLASS.
14+
private final ByteBuffer buffer;
15+
private final RandomAccessFile raf;
16+
private final FileChannel fc;
17+
18+
ThreadBuffer(File database, FileMode mode) throws IOException {
19+
this.raf = new RandomAccessFile(database, "r");
20+
this.fc = this.raf.getChannel();
21+
if (mode == FileMode.IN_MEMORY) {
22+
this.buffer = ByteBuffer.wrap(new byte[(int) this.fc.size()]);
23+
this.fc.read(this.buffer);
24+
} else {
25+
this.buffer = this.fc.map(MapMode.READ_ONLY, 0, this.fc.size());
26+
}
27+
}
28+
29+
// This is just to ease unit testing
30+
ThreadBuffer(ByteBuffer buffer) {
31+
this.buffer = buffer;
32+
this.raf = null;
33+
this.fc = null;
34+
}
35+
36+
@Override
37+
protected synchronized ByteBuffer initialValue() {
38+
return this.buffer.duplicate();
39+
}
40+
41+
public void close() throws IOException {
42+
if (this.fc != null) {
43+
this.fc.close();
44+
}
45+
if (this.raf != null) {
46+
this.raf.close();
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)