Network/test/dorkboxTest/network/lmdb/TutorialTest.kt

487 lines
20 KiB
Kotlin

/*-
* #%L
* LmdbJava
* %%
* Copyright (C) 2016 - 2020 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.
* #L%
*/
package dorkboxTest.network.lmdb
import org.agrona.MutableDirectBuffer
import org.agrona.concurrent.UnsafeBuffer
import org.hamcrest.CoreMatchers
import org.hamcrest.MatcherAssert
import org.junit.Assert
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.lmdbjava.ByteBufferProxy
import org.lmdbjava.DbiFlags
import org.lmdbjava.DirectBufferProxy
import org.lmdbjava.Env
import org.lmdbjava.GetOp
import org.lmdbjava.KeyRange
import org.lmdbjava.SeekOp
import org.lmdbjava.Verifier
import java.io.File
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
/**
* Welcome to LmdbJava!
*
*
*
* This short tutorial will walk you through using LmdbJava step-by-step.
*
*
*
* If you are using a 64-bit Windows, Linux or OS X machine, you can simply run
* this tutorial by adding the LmdbJava JAR to your classpath. It includes the
* required system libraries. If you are using another 64-bit platform, you'll
* need to install the LMDB system library yourself. 32-bit platforms are not
* supported.
*/
@Ignore
class TutorialTest {
private val folder = TemporaryFolder()
@Rule
fun tmp(): TemporaryFolder {
return folder
}
/**
* In this first tutorial we will use LmdbJava with some basic defaults.
*
* @throws IOException if a path was unavailable for memory mapping
*/
@Test
@Throws(IOException::class)
fun tutorial1() {
// We need a storage directory first.
// The path cannot be on a remote file system.
val path = tmp().newFolder()
// We always need an Env. An Env owns a physical on-disk storage file. One
// Env can store many different databases (ie sorted maps).
val env = Env.create() // LMDB also needs to know how large our DB might be. Over-estimating is OK.
.setMapSize(10485760) // LMDB also needs to know how many DBs (Dbi) we want to store in this Env.
.setMaxDbs(1) // Now let's open the Env. The same path can be concurrently opened and
// used in different processes, but do not open the same path twice in
// the same process at the same time.
.open(path)
// We need a Dbi for each DB. A Dbi roughly equates to a sorted map. The
// MDB_CREATE flag causes the DB to be created if it doesn't already exist.
val db = env.openDbi(DB_NAME, DbiFlags.MDB_CREATE)
// We want to store some data, so we will need a direct ByteBuffer.
// Note that LMDB keys cannot exceed maxKeySize bytes (511 bytes by default).
// Values can be larger.
val key = ByteBuffer.allocateDirect(env.maxKeySize)
val `val` = ByteBuffer.allocateDirect(700)
key.put("greeting".toByteArray(StandardCharsets.UTF_8)).flip()
`val`.put("Hello world".toByteArray(StandardCharsets.UTF_8)).flip()
val valSize = `val`.remaining()
// Now store it. Dbi.put() internally begins and commits a transaction (Txn).
db.put(key, `val`)
// To fetch any data from LMDB we need a Txn. A Txn is very important in
// LmdbJava because it offers ACID characteristics and internally holds a
// read-only key buffer and read-only value buffer. These read-only buffers
// are always the same two Java objects, but point to different LMDB-managed
// memory as we use Dbi (and Cursor) methods. These read-only buffers remain
// valid only until the Txn is released or the next Dbi or Cursor call. If
// you need data afterwards, you should copy the bytes to your own buffer.
env.txnRead().use { txn ->
val found = db[txn, key]
Assert.assertNotNull(found)
// The fetchedVal is read-only and points to LMDB memory
val fetchedVal = txn.`val`()
MatcherAssert.assertThat(fetchedVal.remaining(), CoreMatchers.`is`(valSize))
// Let's double-check the fetched value is correct
MatcherAssert.assertThat(StandardCharsets.UTF_8.decode(fetchedVal).toString(), CoreMatchers.`is`("Hello world"))
}
// We can also delete. The simplest way is to let Dbi allocate a new Txn...
db.delete(key)
env.txnRead().use { txn -> Assert.assertNull(db[txn, key]) }
env.close()
}
/**
* In this second tutorial we'll learn more about LMDB's ACID Txns.
*
* @throws IOException if a path was unavailable for memory mapping
* @throws InterruptedException if executor shutdown interrupted
*/
@Test
@Throws(IOException::class, InterruptedException::class)
fun tutorial2() {
val env = createSimpleEnv(tmp().newFolder())
val db = env.openDbi(DB_NAME, DbiFlags.MDB_CREATE)
val key = ByteBuffer.allocateDirect(env.maxKeySize)
val `val` = ByteBuffer.allocateDirect(700)
// Let's write and commit "key1" via a Txn. A Txn can include multiple Dbis.
// Note write Txns block other write Txns, due to writes being serialized.
// It's therefore important to avoid unnecessarily long-lived write Txns.
env.txnWrite().use { txn ->
key.put("key1".toByteArray(StandardCharsets.UTF_8)).flip()
`val`.put("lmdb".toByteArray(StandardCharsets.UTF_8)).flip()
db.put(txn, key, `val`)
// We can read data too, even though this is a write Txn.
val found = db[txn, key]
Assert.assertNotNull(found)
// An explicit commit is required, otherwise Txn.close() rolls it back.
txn.commit()
}
// Open a read-only Txn. It only sees data that existed at Txn creation time.
val rtx = env.txnRead()
// Our read Txn can fetch key1 without problem, as it existed at Txn creation.
var found = db[rtx, key]
Assert.assertNotNull(found)
// Note that our main test thread holds the Txn. Only one Txn per thread is
// typically permitted (the exception is a read-only Env with MDB_NOTLS).
//
// Let's write out a "key2" via a new write Txn in a different thread.
val es = Executors.newCachedThreadPool()
es.execute {
env.txnWrite().use { txn ->
key.clear()
key.put("key2".toByteArray(StandardCharsets.UTF_8)).flip()
db.put(txn, key, `val`)
txn.commit()
}
}
es.shutdown()
es.awaitTermination(10, TimeUnit.SECONDS)
// Even though key2 has been committed, our read Txn still can't see it.
found = db[rtx, key]
Assert.assertNull(found)
// To see key2, we could create a new Txn. But a reset/renew is much faster.
// Reset/renew is also important to avoid long-lived read Txns, as these
// prevent the re-use of free pages by write Txns (ie the DB will grow).
rtx.reset()
// ... potentially long operation here ...
rtx.renew()
found = db[rtx, key]
Assert.assertNotNull(found)
// Don't forget to close the read Txn now we're completely finished. We could
// have avoided this if we used a try-with-resources block, but we wanted to
// play around with multiple concurrent Txns to demonstrate the "I" in ACID.
rtx.close()
env.close()
}
/**
* In this third tutorial we'll have a look at the Cursor. Up until now we've
* just used Dbi, which is good enough for simple cases but unsuitable if you
* don't know the key to fetch, or want to iterate over all the data etc.
*
* @throws IOException if a path was unavailable for memory mapping
*/
@Test
@Throws(IOException::class)
fun tutorial3() {
val env = createSimpleEnv(tmp().newFolder())
val db = env.openDbi(DB_NAME, DbiFlags.MDB_CREATE)
val key = ByteBuffer.allocateDirect(env.maxKeySize)
val `val` = ByteBuffer.allocateDirect(700)
env.txnWrite().use { txn ->
// A cursor always belongs to a particular Dbi.
val c = db.openCursor(txn)
// We can put via a Cursor. Note we're adding keys in a strange order,
// as we want to show you that LMDB returns them in sorted order.
key.put("zzz".toByteArray(StandardCharsets.UTF_8)).flip()
`val`.put("lmdb".toByteArray(StandardCharsets.UTF_8)).flip()
c.put(key, `val`)
key.clear()
key.put("aaa".toByteArray(StandardCharsets.UTF_8)).flip()
c.put(key, `val`)
key.clear()
key.put("ccc".toByteArray(StandardCharsets.UTF_8)).flip()
c.put(key, `val`)
// We can read from the Cursor by key.
c[key, GetOp.MDB_SET]
MatcherAssert.assertThat(StandardCharsets.UTF_8.decode(c.key()).toString(), CoreMatchers.`is`("ccc"))
// Let's see that LMDB provides the keys in appropriate order....
c.seek(SeekOp.MDB_FIRST)
MatcherAssert.assertThat(StandardCharsets.UTF_8.decode(c.key()).toString(), CoreMatchers.`is`("aaa"))
c.seek(SeekOp.MDB_LAST)
MatcherAssert.assertThat(StandardCharsets.UTF_8.decode(c.key()).toString(), CoreMatchers.`is`("zzz"))
c.seek(SeekOp.MDB_PREV)
MatcherAssert.assertThat(StandardCharsets.UTF_8.decode(c.key()).toString(), CoreMatchers.`is`("ccc"))
// Cursors can also delete the current key.
c.delete()
c.close()
txn.commit()
}
// A read-only Cursor can survive its original Txn being closed. This is
// useful if you want to close the original Txn (eg maybe you created the
// Cursor during the constructor of a singleton with a throw-away Txn). Of
// course, you cannot use the Cursor if its Txn is closed or currently reset.
val tx1 = env.txnRead()
val c = db.openCursor(tx1)
tx1.close()
// The Cursor becomes usable again by "renewing" it with an active read Txn.
val tx2 = env.txnRead()
c.renew(tx2)
c.seek(SeekOp.MDB_FIRST)
// As usual with read Txns, we can reset and renew them. The Cursor does
// not need any special handling if we do this.
tx2.reset()
// ... potentially long operation here ...
tx2.renew()
c.seek(SeekOp.MDB_LAST)
tx2.close()
env.close()
}
/**
* In this fourth tutorial we'll take a quick look at the iterators. These are
* a more Java idiomatic form of using the Cursors we looked at in tutorial 3.
*
* @throws IOException if a path was unavailable for memory mapping
*/
@Test
@Throws(IOException::class)
fun tutorial4() {
val env = createSimpleEnv(tmp().newFolder())
val db = env.openDbi(DB_NAME, DbiFlags.MDB_CREATE)
env.txnWrite().use { txn ->
val key = ByteBuffer.allocateDirect(env.maxKeySize)
val `val` = ByteBuffer.allocateDirect(700)
// Insert some data. Note that ByteBuffer order defaults to Big Endian.
// LMDB does not persist the byte order, but it's critical to sort keys.
// If your numeric keys don't sort as expected, review buffer byte order.
`val`.putInt(100)
key.putInt(1)
db.put(txn, key, `val`)
key.clear()
key.putInt(2)
db.put(txn, key, `val`)
key.clear()
// Each iterable uses a cursor and must be closed when finished. Iterate
// forward in terms of key ordering starting with the first key.
db.iterate(txn, KeyRange.all()).use { ci ->
for (kv in ci) {
MatcherAssert.assertThat(kv.key(), CoreMatchers.notNullValue())
MatcherAssert.assertThat(kv.`val`(), CoreMatchers.notNullValue())
}
}
// Iterate backward in terms of key ordering starting with the last key.
db.iterate(txn, KeyRange.allBackward()).use { ci ->
for (kv in ci) {
MatcherAssert.assertThat(kv.key(), CoreMatchers.notNullValue())
MatcherAssert.assertThat(kv.`val`(), CoreMatchers.notNullValue())
}
}
// There are many ways to control the desired key range via KeyRange, such
// as arbitrary start and stop values, direction etc. We've adopted Guava's
// terminology for our range classes (see KeyRangeType for further details).
key.putInt(1)
val range = KeyRange.atLeastBackward(key)
db.iterate(txn, range).use { ci ->
for (kv in ci) {
MatcherAssert.assertThat(kv.key(), CoreMatchers.notNullValue())
MatcherAssert.assertThat(kv.`val`(), CoreMatchers.notNullValue())
}
}
}
env.close()
}
/**
* In this fifth tutorial we'll explore multiple values sharing a single key.
*
* @throws IOException if a path was unavailable for memory mapping
*/
@Test
@Throws(IOException::class)
fun tutorial5() {
val env = createSimpleEnv(tmp().newFolder())
// This time we're going to tell the Dbi it can store > 1 value per key.
// There are other flags available if we're storing integers etc.
val db = env.openDbi(DB_NAME, DbiFlags.MDB_CREATE, DbiFlags.MDB_DUPSORT)
// Duplicate support requires both keys and values to be <= max key size.
val key = ByteBuffer.allocateDirect(env.maxKeySize)
val `val` = ByteBuffer.allocateDirect(env.maxKeySize)
env.txnWrite().use { txn ->
val c = db.openCursor(txn)
// Store one key, but many values, and in non-natural order.
key.put("key".toByteArray(StandardCharsets.UTF_8)).flip()
`val`.put("xxx".toByteArray(StandardCharsets.UTF_8)).flip()
c.put(key, `val`)
`val`.clear()
`val`.put("kkk".toByteArray(StandardCharsets.UTF_8)).flip()
c.put(key, `val`)
`val`.clear()
`val`.put("lll".toByteArray(StandardCharsets.UTF_8)).flip()
c.put(key, `val`)
// Cursor can tell us how many values the current key has.
val count = c.count()
MatcherAssert.assertThat(count, CoreMatchers.`is`(3L))
// Let's position the Cursor. Note sorting still works.
c.seek(SeekOp.MDB_FIRST)
MatcherAssert.assertThat(StandardCharsets.UTF_8.decode(c.`val`()).toString(), CoreMatchers.`is`("kkk"))
c.seek(SeekOp.MDB_LAST)
MatcherAssert.assertThat(StandardCharsets.UTF_8.decode(c.`val`()).toString(), CoreMatchers.`is`("xxx"))
c.seek(SeekOp.MDB_PREV)
MatcherAssert.assertThat(StandardCharsets.UTF_8.decode(c.`val`()).toString(), CoreMatchers.`is`("lll"))
c.close()
txn.commit()
}
env.close()
}
/**
* Next up we'll show you how to easily check your platform (operating system
* and Java version) is working properly with LmdbJava and the embedded LMDB
* native library.
*
* @throws IOException if a path was unavailable for memory mapping
*/
@Test
@Throws(IOException::class)
fun tutorial6() {
// Note we need to specify the Verifier's DBI_COUNT for the Env.
val env = Env.create(ByteBufferProxy.PROXY_OPTIMAL).setMapSize(10485760).setMaxDbs(Verifier.DBI_COUNT).open(tmp().newFolder())
// Create a Verifier (it's a Callable<Long> for those needing full control).
val v = Verifier(env)
// We now run the verifier for 3 seconds; it raises an exception on failure.
// The method returns the number of entries it successfully verified.
v.runFor(3, TimeUnit.SECONDS)
env.close()
}
/**
* In this final tutorial we'll look at using Agrona's DirectBuffer.
*
* @throws IOException if a path was unavailable for memory mapping
*/
@Test
@Throws(IOException::class)
fun tutorial7() {
// The critical difference is we pass the PROXY_DB field to Env.create().
// There's also a PROXY_SAFE if you want to stop ByteBuffer's Unsafe use.
// Aside from that and a different type argument, it's the same as usual...
val env = Env.create(DirectBufferProxy.PROXY_DB).setMapSize(10485760).setMaxDbs(1).open(tmp().newFolder())
val db = env.openDbi(DB_NAME, DbiFlags.MDB_CREATE)
val keyBb = ByteBuffer.allocateDirect(env.maxKeySize)
val key: MutableDirectBuffer = UnsafeBuffer(keyBb)
val `val`: MutableDirectBuffer = UnsafeBuffer(ByteBuffer.allocateDirect(700))
env.txnWrite().use { txn ->
db.openCursor(txn).use { c ->
// Agrona is faster than ByteBuffer and its methods are nicer...
`val`.putStringWithoutLengthUtf8(0, "The Value")
key.putStringWithoutLengthUtf8(0, "yyy")
c.put(key, `val`)
key.putStringWithoutLengthUtf8(0, "ggg")
c.put(key, `val`)
c.seek(SeekOp.MDB_FIRST)
MatcherAssert.assertThat(c.key().getStringWithoutLengthUtf8(0, env.maxKeySize), CoreMatchers.startsWith("ggg"))
c.seek(SeekOp.MDB_LAST)
MatcherAssert.assertThat(c.key().getStringWithoutLengthUtf8(0, env.maxKeySize), CoreMatchers.startsWith("yyy"))
// DirectBuffer has no position concept. Often you don't want to store
// the unnecessary bytes of a varying-size buffer. Let's have a look...
val keyLen = key.putStringWithoutLengthUtf8(0, "12characters")
MatcherAssert.assertThat(keyLen, CoreMatchers.`is`(12))
MatcherAssert.assertThat(key.capacity(), CoreMatchers.`is`(env.maxKeySize))
// To only store the 12 characters, we simply call wrap:
key.wrap(key, 0, keyLen)
MatcherAssert.assertThat(key.capacity(), CoreMatchers.`is`(keyLen))
c.put(key, `val`)
c.seek(SeekOp.MDB_FIRST)
MatcherAssert.assertThat(c.key().capacity(), CoreMatchers.`is`(keyLen))
MatcherAssert.assertThat(c.key().getStringWithoutLengthUtf8(0, c.key().capacity()), CoreMatchers.`is`("12characters"))
// To store bigger values again, just wrap the original buffer.
key.wrap(keyBb)
MatcherAssert.assertThat(key.capacity(), CoreMatchers.`is`(env.maxKeySize))
}
txn.commit()
}
env.close()
}
// You've finished! There are lots of other neat things we could show you (eg
// how to speed up inserts by appending them in key order, using integer
// or reverse ordered keys, using Env.DISABLE_CHECKS_PROP etc), but you now
// know enough to tackle the JavaDocs with confidence. Have fun!
private fun createSimpleEnv(path: File): Env<ByteBuffer> {
return Env.create().setMapSize(10485760).setMaxDbs(1).setMaxReaders(1).open(path)
}
companion object {
private const val DB_NAME = "my DB"
}
}