GitHub Viewer
/*
* Copyright © 2016-2025 The LmdbJava Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lmdbjava;
import static java.nio.ByteBuffer.allocateDirect;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.lmdbjava.CopyFlags.MDB_CP_COMPACT;
import static org.lmdbjava.DbiFlags.MDB_CREATE;
import static org.lmdbjava.Env.Builder.MAX_READERS_DEFAULT;
import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR;
import static org.lmdbjava.EnvFlags.MDB_NOSYNC;
import static org.lmdbjava.EnvFlags.MDB_NOTLS;
import static org.lmdbjava.EnvFlags.MDB_RDONLY_ENV;
import static org.lmdbjava.TestUtils.DB_1;
import static org.lmdbjava.TestUtils.bb;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.lmdbjava.Env.AlreadyClosedException;
import org.lmdbjava.Env.AlreadyOpenException;
import org.lmdbjava.Env.Builder;
import org.lmdbjava.Env.InvalidCopyDestination;
import org.lmdbjava.Env.MapFullException;
import org.lmdbjava.Txn.BadReaderLockException;
/** Test {@link Env}. */
public final class EnvTest {
private TempDir tempDir;
@BeforeEach
void beforeEach() {
tempDir = new TempDir();
}
@AfterEach
void afterEach() {
tempDir.cleanup();
}
@Test
void byteUnit() {
final Path file = tempDir.createTempFile();
try (Env env =
Env.create()
.setMaxReaders(1)
.setMapSize(1, ByteUnit.MEBIBYTES)
.setEnvFlags(MDB_NOSUBDIR)
.open(file)) {
final EnvInfo info = env.info();
assertThat(info.mapSize).isEqualTo(ByteUnit.MEBIBYTES.toBytes(1));
}
}
@Test
void cannotChangeMapSizeAfterOpen() {
assertThatThrownBy(
() -> {
final Path file = tempDir.createTempFile();
final Builder builder =
Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR);
try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) {
builder.setMapSize(1);
}
})
.isInstanceOf(AlreadyOpenException.class);
}
@Test
void cannotChangePermissionsAfterOpen() {
assertThatThrownBy(
() -> {
final Path file = tempDir.createTempFile();
final Builder builder =
Env.create().setFilePermissions(0666).setEnvFlags(MDB_NOSUBDIR);
try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) {
builder.setFilePermissions(0664);
}
})
.isInstanceOf(AlreadyOpenException.class);
}
@Test
void cannotChangeMaxDbsAfterOpen() {
assertThatThrownBy(
() -> {
final Path file = tempDir.createTempFile();
final Builder builder =
Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR);
try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) {
builder.setMaxDbs(1);
}
})
.isInstanceOf(AlreadyOpenException.class);
}
@Test
void cannotChangeMaxReadersAfterOpen() {
assertThatThrownBy(
() -> {
final Path file = tempDir.createTempFile();
final Builder builder =
Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR);
try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) {
builder.setMaxReaders(1);
}
})
.isInstanceOf(AlreadyOpenException.class);
}
@Test
void cannotInfoOnceClosed() {
assertThatThrownBy(
() -> {
final Path file = tempDir.createTempFile();
final Env env =
Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file);
env.close();
env.info();
})
.isInstanceOf(AlreadyClosedException.class);
}
@Test
void cannotOpenTwice() {
assertThatThrownBy(
() -> {
final Path file = tempDir.createTempFile();
final Builder builder =
Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR);
builder.open(file).close();
//noinspection resource // This will fail to open
builder.open(file);
})
.isInstanceOf(AlreadyOpenException.class);
}
@Test
void cannotOverflowMapSize() {
assertThatThrownBy(
() -> {
final Builder builder = Env.create().setMaxReaders(1);
final int mb = 1_024 * 1_024;
//noinspection NumericOverflow // Intentional overflow
final int size = mb * 2_048; // as per issue 18
builder.setMapSize(size);
})
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void negativeMapSize() {
assertThatThrownBy(
() -> {
final Builder builder = Env.create().setMaxReaders(1);
builder.setMapSize(-1);
})
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void negativeMapSize2() {
assertThatThrownBy(
() -> {
final Builder builder = Env.create().setMaxReaders(1);
builder.setMapSize(-1, ByteUnit.MEBIBYTES);
})
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void cannotStatOnceClosed() {
assertThatThrownBy(
() -> {
final Path file = tempDir.createTempFile();
final Env env =
Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file);
env.close();
env.stat();
})
.isInstanceOf(AlreadyClosedException.class);
}
@Test
void cannotSyncOnceClosed() {
assertThatThrownBy(
() -> {
final Path file = tempDir.createTempFile();
final Env env =
Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file);
env.close();
env.sync(false);
})
.isInstanceOf(AlreadyClosedException.class);
}
@Test
void copyDirectoryBased() {
final Path dest = tempDir.createTempDir();
assertThat(Files.exists(dest)).isTrue();
assertThat(Files.isDirectory(dest)).isTrue();
assertThat(FileUtil.count(dest)).isEqualTo(0);
final Path src = tempDir.createTempDir();
try (Env env = Env.create().setMaxReaders(1).open(src)) {
env.copy(dest, MDB_CP_COMPACT);
assertThat(FileUtil.count(dest)).isEqualTo(1);
}
}
@Test
void copyDirectoryBased_noFlags() {
final Path dest = tempDir.createTempDir();
assertThat(Files.exists(dest)).isTrue();
assertThat(Files.isDirectory(dest)).isTrue();
assertThat(FileUtil.count(dest)).isEqualTo(0);
final Path src = tempDir.createTempDir();
try (Env env = Env.create().setMaxReaders(1).open(src)) {
env.copy(dest);
assertThat(FileUtil.count(dest)).isEqualTo(1);
}
}
@Test
void copyDirectoryRejectsFileDestination() {
assertThatThrownBy(
() -> {
final Path dest = tempDir.createTempDir();
FileUtil.deleteDir(dest);
final Path src = tempDir.createTempDir();
try (Env env = Env.create().setMaxReaders(1).open(src)) {
env.copy(dest, MDB_CP_COMPACT);
}
})
.isInstanceOf(InvalidCopyDestination.class);
}
@Test
void copyDirectoryRejectsMissingDestination() {
assertThatThrownBy(
() -> {
final Path dest = tempDir.createTempDir();
try {
Files.delete(dest);
final Path src = tempDir.createTempDir();
try (Env env = Env.create().setMaxReaders(1).open(src)) {
env.copy(dest, MDB_CP_COMPACT);
}
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
})
.isInstanceOf(InvalidCopyDestination.class);
}
@Test
void copyDirectoryRejectsNonEmptyDestination() {
assertThatThrownBy(
() -> {
final Path dest = tempDir.createTempDir();
try {
final Path subDir = dest.resolve("hello");
Files.createDirectory(subDir);
assertThat(Files.isDirectory(subDir)).isTrue();
final Path src = tempDir.createTempDir();
try (Env env = Env.create().setMaxReaders(1).open(src)) {
env.copy(dest, MDB_CP_COMPACT);
}
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
})
.isInstanceOf(InvalidCopyDestination.class);
}
@Test
void copyFileBased() {
final Path dest = tempDir.createTempFile();
assertThat(Files.exists(dest)).isFalse();
final Path src = tempDir.createTempFile();
try (Env env = Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) {
env.copy(dest, MDB_CP_COMPACT);
}
assertThat(FileUtil.size(dest)).isGreaterThan(0L);
}
@Test
void copyFileRejectsExistingDestination() {
assertThatThrownBy(
() -> {
final Path dest = tempDir.createTempFile();
Files.createFile(dest);
assertThat(Files.exists(dest)).isTrue();
final Path src = tempDir.createTempFile();
try (Env env =
Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) {
env.copy(dest, MDB_CP_COMPACT);
}
})
.isInstanceOf(InvalidCopyDestination.class);
}
@Test
void createAsDirectory() {
final Path dest = tempDir.createTempDir();
final Env env = Env.create().setMaxReaders(1).open(dest);
assertThat(Files.isDirectory(dest)).isTrue();
env.sync(false);
env.close();
assertThat(env.isClosed()).isTrue();
env.close(); // safe to repeat
}
@Test
void createAsFile() {
final Path file = tempDir.createTempFile();
try (Env env =
Env.create()
.setMapSize(1, ByteUnit.MEBIBYTES)
.setMaxDbs(1)
.setMaxReaders(1)
.setEnvFlags(MDB_NOSUBDIR)
.open(file)) {
env.sync(true);
assertThat(Files.isRegularFile(file)).isTrue();
}
}
@Test
void detectTransactionThreadViolation() {
assertThatThrownBy(
() -> {
final Path file = tempDir.createTempFile();
try (Env env =
Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) {
env.txnRead();
env.txnRead();
}
})
.isInstanceOf(BadReaderLockException.class);
}
@Test
void info() {
final Path file = tempDir.createTempFile();
try (Env env =
Env.create()
.setMaxReaders(4)
.setMapSize(123_456)
.setEnvFlags(MDB_NOSUBDIR)
.setEnvFlags(MDB_NOSUBDIR)
.open(file)) {
final EnvInfo info = env.info();
assertThat(info).isNotNull();
assertThat(info.lastPageNumber).isEqualTo(1L);
assertThat(info.lastTransactionId).isEqualTo(0L);
assertThat(info.mapAddress).isEqualTo(0L);
assertThat(info.mapSize).isEqualTo(123_456L);
assertThat(info.maxReaders).isEqualTo(4);
assertThat(info.numReaders).isEqualTo(0);
assertThat(info.toString()).contains("maxReaders=");
assertThat(env.getMaxKeySize()).isEqualTo(511);
}
}
@Test
void mapFull() {
assertThatThrownBy(
() -> {
final Path dir = tempDir.createTempDir();
final byte[] k = new byte[500];
final ByteBuffer key = allocateDirect(500);
final ByteBuffer val = allocateDirect(1_024);
final Random rnd = new Random();
try (Env env =
Env.create()
.setMaxReaders(1)
.setMapSize(8, ByteUnit.MEBIBYTES)
.setMaxDbs(1)
.open(dir)) {
final Dbi db =
env.createDbi()
.setDbName(DB_1)
.withDefaultComparator()
.setDbiFlags(MDB_CREATE)
.open();
//noinspection InfiniteLoopStatement // Needs infinite loop to fill the env
for (; ; ) {
rnd.nextBytes(k);
key.clear();
key.put(k).flip();
val.clear();
db.put(key, val);
}
}
})
.isInstanceOf(MapFullException.class);
}
@Test
void readOnlySupported() {
final Path dir = tempDir.createTempDir();
try (Env rwEnv = Env.create().setMaxReaders(1).open(dir)) {
final Dbi rwDb =
rwEnv.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open();
rwDb.put(bb(1), bb(42));
}
try (Env roEnv =
Env.create().setMaxReaders(1).setEnvFlags(MDB_RDONLY_ENV).open(dir)) {
final Dbi roDb =
roEnv
.createDbi()
.setDbName(DB_1)
.withDefaultComparator()
.setDbiFlags(DbiFlagSet.EMPTY)
.open();
try (Txn roTxn = roEnv.txnRead()) {
assertThat(roDb.get(roTxn, bb(1))).isNotNull();
}
}
}
@Test
void setMapSize() {
final Path dir = tempDir.createTempDir();
final byte[] k = new byte[500];
final ByteBuffer key = allocateDirect(500);
final ByteBuffer val = allocateDirect(1_024);
final Random rnd = new Random();
try (Env env =
Env.create().setMaxReaders(1).setMapSize(256, ByteUnit.KIBIBYTES).setMaxDbs(1).open(dir)) {
final Dbi db =
env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open();
db.put(bb(1), bb(42));
boolean mapFullExThrown = false;
try {
for (int i = 0; i < 70; i++) {
rnd.nextBytes(k);
key.clear();
key.put(k).flip();
val.clear();
db.put(key, val);
}
} catch (final MapFullException mfE) {
mapFullExThrown = true;
}
assertThat(mapFullExThrown).isTrue();
assertThatThrownBy(
() -> {
env.setMapSize(-1, ByteUnit.KIBIBYTES);
})
.isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(
() -> {
env.setMapSize(-1);
})
.isInstanceOf(IllegalArgumentException.class);
env.setMapSize(1024, ByteUnit.KIBIBYTES);
try (Txn roTxn = env.txnRead()) {
final ByteBuffer byteBuffer = db.get(roTxn, bb(1));
assertThat(byteBuffer).isNotNull();
assertThat(byteBuffer.getInt()).isEqualTo(42);
}
mapFullExThrown = false;
try {
for (int i = 0; i < 70; i++) {
rnd.nextBytes(k);
key.clear();
key.put(k).flip();
val.clear();
db.put(key, val);
}
} catch (final MapFullException mfE) {
mapFullExThrown = true;
}
assertThat(mapFullExThrown).isFalse();
}
}
@Test
void stats() {
final Path file = tempDir.createTempFile();
try (Env env = Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) {
final Stat stat = env.stat();
assertThat(stat).isNotNull();
assertThat(stat.branchPages).isEqualTo(0L);
assertThat(stat.depth).isEqualTo(0);
assertThat(stat.entries).isEqualTo(0L);
assertThat(stat.leafPages).isEqualTo(0L);
assertThat(stat.overflowPages).isEqualTo(0L);
assertThat(stat.pageSize % 4_096).isEqualTo(0);
assertThat(stat.toString()).contains("pageSize=");
}
}
@Test
void testDefaultOpen() {
final Path dir = tempDir.createTempDir();
try (Env env = Env.create().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) {
final EnvInfo info = env.info();
assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT);
final Dbi db =
env.createDbi().setDbName("test").withDefaultComparator().setDbiFlags(MDB_CREATE).open();
db.put(allocateDirect(1), allocateDirect(1));
}
}
@Test
void testDefaultOpenNoName1() {
final Path dir = tempDir.createTempDir();
try (Env env = Env.create().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) {
final EnvInfo info = env.info();
assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT);
final Dbi db =
env.createDbi()
.setDbName((String) null)
.withDefaultComparator()
.setDbiFlags(MDB_CREATE)
.open();
db.put(bb("abc"), allocateDirect(1));
db.put(bb("def"), allocateDirect(1));
// As this is the unnamed database it returns all keys in the unnamed db
final List dbiNamesBytes = env.getDbiNames();
assertThat(dbiNamesBytes).hasSize(2);
assertThat(dbiNamesBytes.get(0)).isEqualTo("abc".getBytes(Env.DEFAULT_NAME_CHARSET));
assertThat(dbiNamesBytes.get(1)).isEqualTo("def".getBytes(Env.DEFAULT_NAME_CHARSET));
final List dbiNames = env.getDbiNames(Env.DEFAULT_NAME_CHARSET);
assertThat(dbiNames).hasSize(2);
assertThat(dbiNames.get(0)).isEqualTo("abc");
assertThat(dbiNames.get(1)).isEqualTo("def");
}
}
@Test
void testDefaultOpenNoName2() {
final Path dir = tempDir.createTempDir();
try (Env env = Env.create().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) {
final EnvInfo info = env.info();
assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT);
final Dbi db =
env.createDbi()
.setDbName((byte[]) null)
.withDefaultComparator()
.setDbiFlags(MDB_CREATE)
.open();
db.put(bb("abc"), allocateDirect(1));
db.put(bb("def"), allocateDirect(1));
// As this is the unnamed database it returns all keys in the unnamed db
final List dbiNames = env.getDbiNames();
assertThat(dbiNames).hasSize(2);
assertThat(dbiNames.get(0)).isEqualTo("abc".getBytes(Env.DEFAULT_NAME_CHARSET));
assertThat(dbiNames.get(1)).isEqualTo("def".getBytes(Env.DEFAULT_NAME_CHARSET));
}
}
@Test
void addEnvFlag() {
final Path file = tempDir.createTempFile();
try (Env env =
Env.create()
.setMapSize(1, ByteUnit.MEBIBYTES)
.setMaxDbs(1)
.setMaxReaders(1)
.addEnvFlag(MDB_NOSUBDIR)
.addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one
.open(file)) {
env.sync(true);
assertThat(Files.isRegularFile(file)).isTrue();
assertThat(env.getEnvFlagSet().getFlags())
.containsExactlyInAnyOrderElementsOf(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS).getFlags());
}
}
@Test
void addEnvFlags() {
final Path file = tempDir.createTempFile();
try (Env env =
Env.create()
.setMapSize(1, ByteUnit.MEBIBYTES)
.setMaxDbs(1)
.setMaxReaders(1)
.addEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS))
.addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one
.addEnvFlag(null) // no-op
.addEnvFlags((EnvFlagSet) null) // no-op
.addEnvFlags((Collection) null) // no-op
.open(file)) {
env.sync(true);
assertThat(env.getEnvFlagSet().getFlags())
.containsExactlyInAnyOrderElementsOf(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS).getFlags());
assertThat(Files.isRegularFile(file)).isTrue();
}
}
@Test
void addEnvFlags2() {
final Path file = tempDir.createTempFile();
try (Env env =
Env.create()
.setMapSize(1, ByteUnit.MEBIBYTES)
.setMaxDbs(1)
.setMaxReaders(1)
.addEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS))
.addEnvFlags(Collections.singleton(MDB_NOSYNC))
.open(file)) {
env.sync(true);
assertThat(env.getEnvFlagSet().getFlags())
.containsExactlyInAnyOrderElementsOf(
EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS, MDB_NOSYNC).getFlags());
assertThat(Files.isRegularFile(file)).isTrue();
}
}
@Test
void setEnvFlags() {
final Path file = tempDir.createTempFile();
try (Env env =
Env.create()
.setMapSize(1, ByteUnit.MEBIBYTES)
.setMaxDbs(1)
.setMaxReaders(1)
.setEnvFlags((EnvFlagSet) null) // No-op
.setEnvFlags((EnvFlags) null) // No-op
.setEnvFlags((EnvFlags[]) null) // No-op
.setEnvFlags((Collection) null) // No-op
.setEnvFlags(MDB_NOSYNC) // Will be overwritten
.setEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS))
.open(file)) {
env.sync(true);
assertThat(Files.isRegularFile(file)).isTrue();
assertThat(env.getEnvFlagSet().getFlags())
.containsExactlyInAnyOrderElementsOf(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS).getFlags());
}
}
@Test
void setEnvFlags2() {
final Path dir = tempDir.createTempDir();
try (Env env =
Env.create()
.setMapSize(1, ByteUnit.MEBIBYTES)
.setMaxDbs(1)
.setMaxReaders(1)
.setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS)
.setEnvFlags(Collections.emptySet()) // Clears them
.open(dir)) {
env.sync(true);
assertThat(env.getEnvFlagSet().getFlags()).isEmpty();
assertThat(Files.isDirectory(dir));
}
}
@Test
void setEnvFlags_null1() {
final Path file = tempDir.createTempFile();
// MDB_NOSUBDIR is cleared out so it will error as file is a file not a dir
Assertions.assertThatThrownBy(
() -> {
try (Env env =
Env.create()
.setMapSize(1, ByteUnit.MEBIBYTES)
.setMaxDbs(1)
.setMaxReaders(1)
.addEnvFlag(MDB_NOSUBDIR)
.setEnvFlags((Collection) null) // Clears the flags
.open(file)) {}
})
.isInstanceOf(LmdbNativeException.class);
}
@Test
void setEnvFlags_null2() {
final Path file = tempDir.createTempFile();
// MDB_NOSUBDIR is cleared out so it will error as file is a file not a dir
Assertions.assertThatThrownBy(
() -> {
try (Env env =
Env.create()
.setMapSize(1, ByteUnit.MEBIBYTES)
.setMaxDbs(1)
.setMaxReaders(1)
.addEnvFlag(MDB_NOSUBDIR)
.setEnvFlags((EnvFlags) null) // Clears the flags
.open(file)) {}
})
.isInstanceOf(LmdbNativeException.class);
}
@Test
void setEnvFlags_null3() {
final Path file = tempDir.createTempFile();
// MDB_NOSUBDIR is cleared out so it will error as file is a file not a dir
Assertions.assertThatThrownBy(
() -> {
try (Env env =
Env.create()
.setMapSize(1, ByteUnit.MEBIBYTES)
.setMaxDbs(1)
.setMaxReaders(1)
.addEnvFlag(MDB_NOSUBDIR)
.setEnvFlags((EnvFlagSet) null) // Clears the flags
.open(file)) {}
})
.isInstanceOf(LmdbNativeException.class);
}
}