Skip to content

Commit 3a2bcc0

Browse files
committed
Merge branch 'gh-1000-max-readers-thread-pool' into dev
2 parents e31e08c + def827d commit 3a2bcc0

File tree

4 files changed

+144
-24
lines changed

4 files changed

+144
-24
lines changed

objectbox-java/src/main/java/io/objectbox/BoxStoreBuilder.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ public class BoxStoreBuilder {
9292
int fileMode;
9393

9494
int maxReaders;
95+
boolean noReaderThreadLocals;
9596

9697
int queryAttempts;
9798

@@ -288,20 +289,36 @@ public BoxStoreBuilder fileMode(int mode) {
288289
}
289290

290291
/**
291-
* Sets the maximum number of concurrent readers. For most applications, the default is fine (> 100 readers).
292+
* Sets the maximum number of concurrent readers. For most applications, the default is fine (~ 126 readers).
292293
* <p>
293294
* A "reader" is short for a thread involved in a read transaction.
294295
* <p>
295296
* If you hit {@link io.objectbox.exception.DbMaxReadersExceededException}, you should first worry about the
296297
* amount of threads you are using.
297298
* For highly concurrent setups (e.g. you are using ObjectBox on the server side) it may make sense to increase the
298299
* number.
300+
*
301+
* Note: Each thread that performed a read transaction and is still alive holds on to a reader slot.
302+
* These slots only get vacated when the thread ends. Thus, be mindful with the number of active threads.
303+
* Alternatively, you can opt to try the experimental noReaderThreadLocals option flag.
299304
*/
300305
public BoxStoreBuilder maxReaders(int maxReaders) {
301306
this.maxReaders = maxReaders;
302307
return this;
303308
}
304309

310+
/**
311+
* Disables the usage of thread locals for "readers" related to read transactions.
312+
* This can make sense if you are using a lot of threads that are kept alive.
313+
*
314+
* Note: This is still experimental, as it comes with subtle behavior changes at a low level and may affect
315+
* corner cases with e.g. transactions, which may not be fully tested at the moment.
316+
*/
317+
public BoxStoreBuilder noReaderThreadLocals() {
318+
this.noReaderThreadLocals = true;
319+
return this;
320+
}
321+
305322
@Internal
306323
public void entity(EntityInfo<?> entityInfo) {
307324
entityInfoList.add(entityInfo);
@@ -477,6 +494,7 @@ byte[] buildFlatStoreOptions(String canonicalPath) {
477494
if(skipReadSchema) FlatStoreOptions.addSkipReadSchema(fbb, skipReadSchema);
478495
if(usePreviousCommit) FlatStoreOptions.addUsePreviousCommit(fbb, usePreviousCommit);
479496
if(readOnly) FlatStoreOptions.addReadOnly(fbb, readOnly);
497+
if(noReaderThreadLocals) FlatStoreOptions.addNoReaderThreadLocals(fbb, noReaderThreadLocals);
480498
if (debugFlags != 0) {
481499
FlatStoreOptions.addDebugFlags(fbb, debugFlags);
482500
}

objectbox-java/src/main/java/io/objectbox/model/FlatStoreOptions.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,19 @@ public final class FlatStoreOptions extends Table {
6464
*/
6565
public long fileMode() { int o = __offset(10); return o != 0 ? (long)bb.getInt(o + bb_pos) & 0xFFFFFFFFL : 0L; }
6666
/**
67-
* The maximum number of readers.
67+
* The maximum number of readers (related to read transactions).
6868
* "Readers" are an finite resource for which we need to define a maximum number upfront.
6969
* The default value is enough for most apps and usually you can ignore it completely.
7070
* However, if you get the OBX_ERROR_MAX_READERS_EXCEEDED error, you should verify your
7171
* threading. For each thread, ObjectBox uses multiple readers. Their number (per thread) depends
7272
* on number of types, relations, and usage patterns. Thus, if you are working with many threads
7373
* (e.g. in a server-like scenario), it can make sense to increase the maximum number of readers.
74-
* Note: The internal default is currently around 120.
75-
* So when hitting this limit, try values around 200-500.
74+
*
75+
* Note: The internal default is currently 126. So when hitting this limit, try value s around 200-500.
76+
*
77+
* Note: Each thread that performed a read transaction and is still alive holds on to a reader slot.
78+
* These slots only get vacated when the thread ends. Thus be mindful with the number of active threads.
79+
* Alternatively, you can opt to try the experimental noReaderThreadLocals option flag.
7680
*/
7781
public long maxReaders() { int o = __offset(12); return o != 0 ? (long)bb.getInt(o + bb_pos) & 0xFFFFFFFFL : 0L; }
7882
/**
@@ -123,6 +127,14 @@ public final class FlatStoreOptions extends Table {
123127
* For debugging purposes you may want enable specific logging.
124128
*/
125129
public long debugFlags() { int o = __offset(28); return o != 0 ? (long)bb.getInt(o + bb_pos) & 0xFFFFFFFFL : 0L; }
130+
/**
131+
* Disables the usage of thread locals for "readers" related to read transactions.
132+
* This can make sense if you are using a lot of threads that are kept alive.
133+
*
134+
* Note: This is still experimental, as it comes with subtle behavior changes at a low level and may affect
135+
* corner cases with e.g. transactions, which may not be fully tested at the moment.
136+
*/
137+
public boolean noReaderThreadLocals() { int o = __offset(30); return o != 0 ? 0!=bb.get(o + bb_pos) : false; }
126138

127139
public static int createFlatStoreOptions(FlatBufferBuilder builder,
128140
int directoryPathOffset,
@@ -137,8 +149,9 @@ public static int createFlatStoreOptions(FlatBufferBuilder builder,
137149
boolean usePreviousCommit,
138150
boolean usePreviousCommitOnValidationFailure,
139151
boolean readOnly,
140-
long debugFlags) {
141-
builder.startTable(13);
152+
long debugFlags,
153+
boolean noReaderThreadLocals) {
154+
builder.startTable(14);
142155
FlatStoreOptions.addValidateOnOpenPageLimit(builder, validateOnOpenPageLimit);
143156
FlatStoreOptions.addMaxDbSizeInKByte(builder, maxDbSizeInKByte);
144157
FlatStoreOptions.addDebugFlags(builder, debugFlags);
@@ -148,14 +161,15 @@ public static int createFlatStoreOptions(FlatBufferBuilder builder,
148161
FlatStoreOptions.addDirectoryPath(builder, directoryPathOffset);
149162
FlatStoreOptions.addPutPaddingMode(builder, putPaddingMode);
150163
FlatStoreOptions.addValidateOnOpen(builder, validateOnOpen);
164+
FlatStoreOptions.addNoReaderThreadLocals(builder, noReaderThreadLocals);
151165
FlatStoreOptions.addReadOnly(builder, readOnly);
152166
FlatStoreOptions.addUsePreviousCommitOnValidationFailure(builder, usePreviousCommitOnValidationFailure);
153167
FlatStoreOptions.addUsePreviousCommit(builder, usePreviousCommit);
154168
FlatStoreOptions.addSkipReadSchema(builder, skipReadSchema);
155169
return FlatStoreOptions.endFlatStoreOptions(builder);
156170
}
157171

158-
public static void startFlatStoreOptions(FlatBufferBuilder builder) { builder.startTable(13); }
172+
public static void startFlatStoreOptions(FlatBufferBuilder builder) { builder.startTable(14); }
159173
public static void addDirectoryPath(FlatBufferBuilder builder, int directoryPathOffset) { builder.addOffset(0, directoryPathOffset, 0); }
160174
public static void addModelBytes(FlatBufferBuilder builder, int modelBytesOffset) { builder.addOffset(1, modelBytesOffset, 0); }
161175
public static int createModelBytesVector(FlatBufferBuilder builder, byte[] data) { return builder.createByteVector(data); }
@@ -172,6 +186,7 @@ public static int createFlatStoreOptions(FlatBufferBuilder builder,
172186
public static void addUsePreviousCommitOnValidationFailure(FlatBufferBuilder builder, boolean usePreviousCommitOnValidationFailure) { builder.addBoolean(10, usePreviousCommitOnValidationFailure, false); }
173187
public static void addReadOnly(FlatBufferBuilder builder, boolean readOnly) { builder.addBoolean(11, readOnly, false); }
174188
public static void addDebugFlags(FlatBufferBuilder builder, long debugFlags) { builder.addInt(12, (int) debugFlags, (int) 0L); }
189+
public static void addNoReaderThreadLocals(FlatBufferBuilder builder, boolean noReaderThreadLocals) { builder.addBoolean(13, noReaderThreadLocals, false); }
175190
public static int endFlatStoreOptions(FlatBufferBuilder builder) {
176191
int o = builder.endTable();
177192
return o;

tests/objectbox-java-test/src/test/java/io/objectbox/TestUtils.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@
3333

3434
public class TestUtils {
3535

36+
public static boolean isWindows() {
37+
final String osName = System.getProperty("os.name").toLowerCase();
38+
return osName.contains("windows");
39+
}
40+
3641
public static String loadFile(String filename) {
3742
try {
3843
InputStream in = openInputStream("/" + filename);

tests/objectbox-java-test/src/test/java/io/objectbox/TransactionTest.java

Lines changed: 99 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,31 @@
1616

1717
package io.objectbox;
1818

19+
import io.objectbox.exception.DbException;
20+
import io.objectbox.exception.DbExceptionListener;
21+
import io.objectbox.exception.DbMaxReadersExceededException;
1922
import org.junit.Ignore;
2023
import org.junit.Test;
2124

25+
import java.util.ArrayList;
2226
import java.util.concurrent.Callable;
2327
import java.util.concurrent.CountDownLatch;
28+
import java.util.concurrent.ExecutorService;
29+
import java.util.concurrent.Executors;
30+
import java.util.concurrent.Future;
2431
import java.util.concurrent.LinkedBlockingQueue;
32+
import java.util.concurrent.ThreadPoolExecutor;
2533
import java.util.concurrent.TimeUnit;
2634
import java.util.concurrent.atomic.AtomicInteger;
2735

28-
import javax.annotation.Nullable;
29-
30-
import io.objectbox.exception.DbException;
31-
import io.objectbox.exception.DbExceptionListener;
32-
import io.objectbox.exception.DbMaxReadersExceededException;
33-
34-
import static org.junit.Assert.*;
36+
import static org.junit.Assert.assertArrayEquals;
37+
import static org.junit.Assert.assertEquals;
38+
import static org.junit.Assert.assertFalse;
39+
import static org.junit.Assert.assertNotNull;
40+
import static org.junit.Assert.assertNotSame;
41+
import static org.junit.Assert.assertSame;
42+
import static org.junit.Assert.assertTrue;
43+
import static org.junit.Assert.fail;
3544

3645
public class TransactionTest extends AbstractObjectBoxTest {
3746

@@ -41,9 +50,9 @@ private void prepareOneEntryWith1230() {
4150
KeyValueCursor cursor = transaction.createKeyValueCursor();
4251
cursor.put(123, new byte[]{1, 2, 3, 0});
4352
cursor.close();
44-
assertEquals(true, transaction.isActive());
53+
assertTrue(transaction.isActive());
4554
transaction.commit();
46-
assertEquals(false, transaction.isActive());
55+
assertFalse(transaction.isActive());
4756
}
4857

4958
@Test
@@ -83,17 +92,17 @@ public void testReadTransactionWhileWriting() {
8392
cursorRead.close();
8493

8594
// commit writing
86-
assertEquals(true, txRead.isReadOnly());
87-
assertEquals(false, txWrite.isReadOnly());
95+
assertTrue(txRead.isReadOnly());
96+
assertFalse(txWrite.isReadOnly());
8897

89-
assertEquals(true, txWrite.isActive());
98+
assertTrue(txWrite.isActive());
9099
txWrite.commit();
91-
assertEquals(false, txWrite.isActive());
100+
assertFalse(txWrite.isActive());
92101

93102
// commit reading
94-
assertEquals(true, txRead.isActive());
103+
assertTrue(txRead.isActive());
95104
txRead.abort();
96-
assertEquals(false, txRead.isActive());
105+
assertFalse(txRead.isActive());
97106

98107
// start reading again and get the new value
99108
txRead = store.beginReadTx();
@@ -118,7 +127,7 @@ public void testTransactionReset() {
118127
assertArrayEquals(new byte[]{3, 2, 1, 0}, cursor.get(123));
119128
cursor.close();
120129
transaction.reset();
121-
assertEquals(true, transaction.isActive());
130+
assertTrue(transaction.isActive());
122131

123132
cursor = transaction.createKeyValueCursor();
124133
assertArrayEquals(new byte[]{1, 2, 3, 0}, cursor.get(123));
@@ -145,7 +154,7 @@ public void testTransactionReset() {
145154
assertArrayEquals(new byte[]{3, 2, 1, 0}, cursor.get(123));
146155
cursor.close();
147156
transaction.reset();
148-
assertEquals(true, transaction.isActive());
157+
assertTrue(transaction.isActive());
149158

150159
cursor = transaction.createKeyValueCursor();
151160
assertArrayEquals(new byte[]{3, 2, 1, 0}, cursor.get(123));
@@ -439,5 +448,78 @@ public void testCallInTxAsync_Error() throws InterruptedException {
439448
assertNotNull(result);
440449
}
441450

451+
@Test
452+
public void transaction_unboundedThreadPool() throws Exception {
453+
runThreadPoolReaderTest(
454+
() -> {
455+
Transaction tx = store.beginReadTx();
456+
tx.close();
457+
}
458+
);
459+
}
460+
461+
@Test
462+
public void runInReadTx_unboundedThreadPool() throws Exception {
463+
runThreadPoolReaderTest(
464+
() -> store.runInReadTx(() -> {
465+
})
466+
);
467+
}
468+
469+
@Test
470+
public void callInReadTx_unboundedThreadPool() throws Exception {
471+
runThreadPoolReaderTest(
472+
() -> store.callInReadTx(() -> 1)
473+
);
474+
}
442475

476+
@Test
477+
public void boxReader_unboundedThreadPool() throws Exception {
478+
runThreadPoolReaderTest(
479+
() -> {
480+
store.boxFor(TestEntity.class).count();
481+
store.closeThreadResources();
482+
}
483+
);
484+
}
485+
486+
/**
487+
* Tests that a reader is available again after a transaction is closed on a thread.
488+
* To not exceed max readers this test simply does not allow any two threads
489+
* to have an active transaction at the same time, e.g. there should always be only one active reader.
490+
*/
491+
private void runThreadPoolReaderTest(Runnable runnable) throws Exception {
492+
// Replace default store: transaction logging disabled and specific max readers.
493+
tearDown();
494+
store = createBoxStoreBuilder(null)
495+
.maxReaders(100)
496+
.debugFlags(0)
497+
.noReaderThreadLocals() // This is the essential flag to make this test work
498+
.build();
499+
500+
// Unbounded (but throttled) thread pool so number of threads run exceeds max readers.
501+
int numThreads = TestUtils.isWindows() ? 300 : 1000; // Less on Windows; had some resource issues on Windows CI
502+
ExecutorService pool = Executors.newCachedThreadPool();
503+
504+
ArrayList<Future<Integer>> txTasks = new ArrayList<>(10000);
505+
final Object lock = new Object();
506+
for (int i = 0; i < 10000; i++) {
507+
final int txNumber = i;
508+
txTasks.add(pool.submit(() -> {
509+
// Lock to ensure no two threads have an active transaction at the same time.
510+
synchronized (lock) {
511+
runnable.run();
512+
return txNumber;
513+
}
514+
}));
515+
if (pool instanceof ThreadPoolExecutor && ((ThreadPoolExecutor) pool).getActiveCount() > numThreads) {
516+
Thread.sleep(1); // Throttle processing to limit thread resources
517+
}
518+
}
519+
520+
//Iterate through all the txTasks and make sure all transactions succeeded.
521+
for (Future<Integer> txTask : txTasks) {
522+
txTask.get(1, TimeUnit.MINUTES); // 1s would be enough for normally, but use 1 min to allow debug sessions
523+
}
524+
}
443525
}

0 commit comments

Comments
 (0)