diff --git a/paimon-common/src/main/java/org/apache/paimon/lookup/sort/db/LocalKvDb.java b/paimon-common/src/main/java/org/apache/paimon/lookup/sort/db/LocalKvDb.java index 6875fc2c31a2..2176e60be326 100644 --- a/paimon-common/src/main/java/org/apache/paimon/lookup/sort/db/LocalKvDb.java +++ b/paimon-common/src/main/java/org/apache/paimon/lookup/sort/db/LocalKvDb.java @@ -26,7 +26,10 @@ import org.apache.paimon.lookup.sort.SortLookupStoreWriter; import org.apache.paimon.memory.MemorySlice; import org.apache.paimon.options.MemorySize; +import org.apache.paimon.sst.BlockIterator; +import org.apache.paimon.sst.SstFileReader; import org.apache.paimon.utils.BloomFilter; +import org.apache.paimon.utils.KeyValueIterator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,12 +39,14 @@ import java.io.Closeable; import java.io.File; import java.io.IOException; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.PriorityQueue; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.ExecutorService; @@ -123,6 +128,7 @@ public class LocalKvDb implements Closeable { private final Map readerCache; private final AtomicLong fileSequence; + private int openRangeIterators; private boolean closed; private LocalKvDb( @@ -147,6 +153,7 @@ private LocalKvDb( this.levels = new LsmLevels(MAX_LEVELS); this.readerCache = new HashMap<>(); this.fileSequence = new AtomicLong(); + this.openRangeIterators = 0; this.closed = false; LsmCompactor.CompactorFactory compactorFactory = fileDeleter -> @@ -214,6 +221,7 @@ public static Builder builder(File dataDirectory) { */ public void put(byte[] key, byte[] value) throws IOException { ensureOpen(); + ensureNoRangeIterator(); checkCompactionFailure(); if (value.length == 0) { throw new IllegalArgumentException( @@ -239,6 +247,7 @@ public void put(byte[] key, byte[] value) throws IOException { */ public void delete(byte[] key) throws IOException { ensureOpen(); + ensureNoRangeIterator(); checkCompactionFailure(); MemorySlice wrappedKey = MemorySlice.wrap(key); byte[] oldValue = memTable.put(wrappedKey, TOMBSTONE); @@ -446,6 +455,76 @@ public byte[] get(byte[] key) throws IOException { return value == null || isTombstone(value) ? null : value; } + /** + * Scan live entries in the half-open range [{@code fromInclusive}, {@code toExclusive}). + * + *

The result is sorted by the configured key comparator. Newer values and tombstones shadow + * older versions in the same way as {@link #get(byte[])}. A null upper bound scans to the end + * of the database. + */ + public List> rangeScan( + byte[] fromInclusive, @Nullable byte[] toExclusive) throws IOException { + List> result = new ArrayList<>(); + forEachInRange( + fromInclusive, + toExclusive, + (key, value) -> + result.add( + new AbstractMap.SimpleImmutableEntry<>( + key.copyBytes(), value.copyBytes()))); + return result; + } + + /** + * Visit live entries in the half-open range [{@code fromInclusive}, {@code toExclusive}). + * + *

Entries are visited in key order with the same shadowing rules as {@link #get(byte[])}. + * The supplied slices are only valid for the duration of the callback and must be copied if + * retained. + */ + public void forEachInRange( + byte[] fromInclusive, @Nullable byte[] toExclusive, RangeEntryConsumer consumer) + throws IOException { + try (RangeIterator iterator = rangeIterator(fromInclusive, toExclusive)) { + while (iterator.advanceNext()) { + consumer.accept(iterator.getKey(), iterator.getValue()); + } + } + } + + /** + * Create a lazy iterator over live entries in the half-open range [{@code fromInclusive}, + * {@code toExclusive}). + * + *

The iterator merges the MemTable and all overlapping SST files in key order. For duplicate + * keys, the newest source wins and tombstones suppress older values. The iterator must be + * closed before modifying or closing the database. Closing releases the levels read lock so a + * concurrent compaction can publish its result. + */ + public RangeIterator rangeIterator(byte[] fromInclusive, @Nullable byte[] toExclusive) + throws IOException { + ensureOpen(); + checkCompactionFailure(); + + MemorySlice from = MemorySlice.wrap(fromInclusive); + MemorySlice to = toExclusive == null ? null : MemorySlice.wrap(toExclusive); + checkArgument( + to == null || keyComparator.compare(from, to) <= 0, + "Range start must not be greater than range end."); + + Map memoryEntries = + to == null ? memTable.tailMap(from, true) : memTable.subMap(from, true, to, false); + LsmLevels.RangeSnapshot snapshot = levels.openRangeSnapshot(from, to, keyComparator); + try { + RangeIterator iterator = new RangeIterator(snapshot, memoryEntries, fromInclusive, to); + openRangeIterators++; + return iterator; + } catch (IOException | RuntimeException e) { + snapshot.close(); + throw e; + } + } + // ------------------------------------------------------------------------- // Flush & Compaction // ------------------------------------------------------------------------- @@ -459,6 +538,7 @@ public byte[] get(byte[] key) throws IOException { */ public void flush() throws IOException { ensureOpen(); + ensureNoRangeIterator(); checkCompactionFailure(); if (memTable.isEmpty()) { return; @@ -491,6 +571,7 @@ private void flushMemTable() throws IOException { */ public void compact() throws IOException { ensureOpen(); + ensureNoRangeIterator(); compaction.fullCompact(); } @@ -503,6 +584,7 @@ public void close() throws IOException { if (closed) { return; } + ensureNoRangeIterator(); closed = true; IOException failure = null; @@ -607,6 +689,15 @@ private byte[] lookupInFile(File file, byte[] key) throws IOException { return reader.lookup(key); } + private SortLookupStoreReader getOrCreateReader(File file) throws IOException { + SortLookupStoreReader reader = readerCache.get(file); + if (reader == null) { + reader = storeFactory.createReader(file); + readerCache.put(file, reader); + } + return reader; + } + private SstFileMetadata writeMemTableToSst(TreeMap data) throws IOException { File sstFile = newSstFile(); @@ -643,6 +734,228 @@ private void ensureOpen() { } } + private void ensureNoRangeIterator() { + if (openRangeIterators > 0) { + throw new IllegalStateException( + "The database cannot be modified or closed while a range iterator is open."); + } + } + + /** Lazy range iterator with newest-version-wins semantics. */ + public final class RangeIterator + implements KeyValueIterator, AutoCloseable { + + private final LsmLevels.RangeSnapshot snapshot; + private final PriorityQueue sources; + + @Nullable private MemorySlice currentKey; + @Nullable private MemorySlice currentValue; + private boolean closed; + + private RangeIterator( + LsmLevels.RangeSnapshot snapshot, + Map memoryEntries, + byte[] fromInclusive, + @Nullable MemorySlice toExclusive) + throws IOException { + this.snapshot = snapshot; + this.sources = + new PriorityQueue<>( + (left, right) -> { + int compare = keyComparator.compare(left.key(), right.key()); + return compare != 0 + ? compare + : Integer.compare(left.priority(), right.priority()); + }); + + int priority = 0; + advanceAndAdd(new MemoryRangeSource(priority++, memoryEntries.entrySet().iterator())); + for (File file : snapshot.files()) { + advanceAndAdd( + new SstRangeSource( + priority++, getOrCreateReader(file), fromInclusive, toExclusive)); + } + } + + @Override + public boolean advanceNext() throws IOException { + if (closed) { + return false; + } + + currentKey = null; + currentValue = null; + try { + while (!sources.isEmpty()) { + RangeSource newest = sources.poll(); + MemorySlice key = newest.key(); + MemorySlice value = newest.value(); + advanceAndAdd(newest); + + while (!sources.isEmpty() + && keyComparator.compare(sources.peek().key(), key) == 0) { + advanceAndAdd(sources.poll()); + } + + if (!isTombstoneSlice(value)) { + currentKey = key; + currentValue = value; + return true; + } + } + close(); + return false; + } catch (IOException | RuntimeException e) { + close(); + throw e; + } + } + + @Override + public MemorySlice getKey() { + if (currentKey == null) { + throw new IllegalStateException("Range iterator is not positioned on an entry."); + } + return currentKey; + } + + @Override + public MemorySlice getValue() { + if (currentValue == null) { + throw new IllegalStateException("Range iterator is not positioned on an entry."); + } + return currentValue; + } + + private void advanceAndAdd(RangeSource source) throws IOException { + if (source.advance()) { + sources.add(source); + } + } + + @Override + public void close() { + if (!closed) { + closed = true; + sources.clear(); + currentKey = null; + currentValue = null; + snapshot.close(); + openRangeIterators--; + } + } + } + + private interface RangeSource { + + int priority(); + + boolean advance() throws IOException; + + MemorySlice key(); + + MemorySlice value(); + } + + private static final class MemoryRangeSource implements RangeSource { + + private final int priority; + private final Iterator> iterator; + + @Nullable private Map.Entry current; + + private MemoryRangeSource(int priority, Iterator> iterator) { + this.priority = priority; + this.iterator = iterator; + } + + @Override + public int priority() { + return priority; + } + + @Override + public boolean advance() { + current = iterator.hasNext() ? iterator.next() : null; + return current != null; + } + + @Override + public MemorySlice key() { + return current.getKey(); + } + + @Override + public MemorySlice value() { + return MemorySlice.wrap(current.getValue()); + } + } + + private final class SstRangeSource implements RangeSource { + + private final int priority; + private final SstFileReader.SstFileIterator iterator; + @Nullable private final MemorySlice toExclusive; + + @Nullable private BlockIterator block; + @Nullable private Map.Entry current; + + private SstRangeSource( + int priority, + SortLookupStoreReader reader, + byte[] fromInclusive, + @Nullable MemorySlice toExclusive) { + this.priority = priority; + this.iterator = reader.createIterator(); + this.toExclusive = toExclusive; + this.iterator.seekTo(fromInclusive); + } + + @Override + public int priority() { + return priority; + } + + @Override + public boolean advance() throws IOException { + while (block == null || !block.hasNext()) { + block = iterator.readBatch(); + if (block == null) { + current = null; + return false; + } + } + + current = block.next(); + if (toExclusive != null && keyComparator.compare(current.getKey(), toExclusive) >= 0) { + current = null; + return false; + } + return true; + } + + @Override + public MemorySlice key() { + return current.getKey(); + } + + @Override + public MemorySlice value() { + return current.getValue(); + } + } + + private static boolean isTombstoneSlice(MemorySlice value) { + return value.length() == 0; + } + + /** Callback for visiting an entry during a range scan. */ + @FunctionalInterface + public interface RangeEntryConsumer { + + void accept(MemorySlice key, MemorySlice value) throws IOException; + } + // ------------------------------------------------------------------------- // Builder // ------------------------------------------------------------------------- diff --git a/paimon-common/src/main/java/org/apache/paimon/lookup/sort/db/LsmLevels.java b/paimon-common/src/main/java/org/apache/paimon/lookup/sort/db/LsmLevels.java index 6f13e08dff72..dfd9bec75560 100644 --- a/paimon-common/src/main/java/org/apache/paimon/lookup/sort/db/LsmLevels.java +++ b/paimon-common/src/main/java/org/apache/paimon/lookup/sort/db/LsmLevels.java @@ -119,6 +119,56 @@ byte[] lookup( } } + /** + * Open a snapshot of all SST files which overlap the requested range. + * + *

Files are ordered from newer levels to older levels; Level-0 files are additionally + * ordered newest first. The read lock remains held until the returned snapshot is closed so a + * concurrent compaction cannot delete a file being iterated. + */ + RangeSnapshot openRangeSnapshot( + MemorySlice fromInclusive, + @Nullable MemorySlice toExclusive, + Comparator keyComparator) { + lock.readLock().lock(); + boolean success = false; + try { + List files = new ArrayList<>(); + for (int level = 0; level < maxLevels; level++) { + List levelFiles = levels.get(level); + if (levelFiles.isEmpty()) { + continue; + } + + if (level == 0) { + for (SstFileMetadata metadata : levelFiles) { + if (overlapsRange(metadata, fromInclusive, toExclusive, keyComparator)) { + files.add(metadata.getFile()); + } + } + continue; + } + + int firstFile = findFirstOverlappingFile(levelFiles, fromInclusive, keyComparator); + for (int i = firstFile; i < levelFiles.size(); i++) { + SstFileMetadata metadata = levelFiles.get(i); + if (toExclusive != null + && keyComparator.compare(metadata.getMinKey(), toExclusive) >= 0) { + break; + } + files.add(metadata.getFile()); + } + } + RangeSnapshot snapshot = new RangeSnapshot(files); + success = true; + return snapshot; + } finally { + if (!success) { + lock.readLock().unlock(); + } + } + } + List> snapshot() { lock.readLock().lock(); try { @@ -265,6 +315,56 @@ private static SstFileMetadata findFileForKey( return null; } + private static int findFirstOverlappingFile( + List sortedFiles, + MemorySlice fromInclusive, + Comparator keyComparator) { + int low = 0; + int high = sortedFiles.size(); + while (low < high) { + int mid = low + (high - low) / 2; + if (keyComparator.compare(sortedFiles.get(mid).getMaxKey(), fromInclusive) < 0) { + low = mid + 1; + } else { + high = mid; + } + } + return low; + } + + private static boolean overlapsRange( + SstFileMetadata metadata, + MemorySlice fromInclusive, + @Nullable MemorySlice toExclusive, + Comparator keyComparator) { + return keyComparator.compare(metadata.getMaxKey(), fromInclusive) >= 0 + && (toExclusive == null + || keyComparator.compare(metadata.getMinKey(), toExclusive) < 0); + } + + /** SST files in a range protected from concurrent compaction until closed. */ + final class RangeSnapshot implements AutoCloseable { + + private final List files; + private boolean closed; + + private RangeSnapshot(List files) { + this.files = files; + } + + List files() { + return files; + } + + @Override + public void close() { + if (!closed) { + closed = true; + lock.readLock().unlock(); + } + } + } + /** Callback for reading one SST file. */ interface FileLookup { diff --git a/paimon-common/src/test/java/org/apache/paimon/lookup/sort/db/LocalKvDbAsyncCompactionTest.java b/paimon-common/src/test/java/org/apache/paimon/lookup/sort/db/LocalKvDbAsyncCompactionTest.java index 205b9e51fa44..ab94fab9e061 100644 --- a/paimon-common/src/test/java/org/apache/paimon/lookup/sort/db/LocalKvDbAsyncCompactionTest.java +++ b/paimon-common/src/test/java/org/apache/paimon/lookup/sort/db/LocalKvDbAsyncCompactionTest.java @@ -26,6 +26,9 @@ import java.io.File; import java.io.IOException; import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import java.util.Queue; import java.util.concurrent.AbstractExecutorService; import java.util.concurrent.CountDownLatch; @@ -54,12 +57,48 @@ void testAutomaticCompactionRunsInBackground() throws Exception { assertThat(compactionExecutor.numQueuedTasks()).isOne(); assertThat(db.getLevelFileCount(0)).isEqualTo(3); assertThat(get(db, "shared")).isEqualTo("v3"); + assertThat(scan(db, "sha", "shb")).containsExactly("shared=v3"); compactionExecutor.runNext(); db.awaitCompaction(); assertThat(db.getLevelFileCount(0)).isZero(); assertThat(get(db, "shared")).isEqualTo("v3"); + assertThat(scan(db, "sha", "shb")).containsExactly("shared=v3"); + } + } + + @Test + void testRangeIteratorPreventsCompactionFromDeletingItsFiles() throws Exception { + ManuallyTriggeredExecutor compactionExecutor = new ManuallyTriggeredExecutor(); + File directory = new File(tempDir.toFile(), "range-compaction"); + try (LocalKvDb db = createDb("range-compaction", compactionExecutor)) { + putAndFlush(db, "shared", "v1"); + putAndFlush(db, "shared", "v2"); + putAndFlush(db, "shared", "v3"); + + LocalKvDb.RangeIterator iterator = + db.rangeIterator("sha".getBytes(UTF_8), "shb".getBytes(UTF_8)); + ExecutorService compactionRunner = Executors.newSingleThreadExecutor(); + Future compactionFuture = compactionRunner.submit(compactionExecutor::runNext); + try { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + while (sstFileCount(directory) < 4 && System.nanoTime() < deadline) { + Thread.yield(); + } + + assertThat(sstFileCount(directory)).isGreaterThanOrEqualTo(4); + assertThat(compactionFuture).isNotDone(); + assertThat(iterator.advanceNext()).isTrue(); + assertThat(new String(iterator.getValue().copyBytes(), UTF_8)).isEqualTo("v3"); + } finally { + iterator.close(); + compactionFuture.get(10, TimeUnit.SECONDS); + compactionRunner.shutdownNow(); + } + + db.awaitCompaction(); + assertThat(scan(db, "sha", "shb")).containsExactly("shared=v3"); } } @@ -208,6 +247,21 @@ private static String get(LocalKvDb db, String key) throws IOException { return value == null ? null : new String(value, UTF_8); } + private static List scan(LocalKvDb db, String from, String to) throws IOException { + List result = new ArrayList<>(); + for (Map.Entry entry : + db.rangeScan(from.getBytes(UTF_8), to.getBytes(UTF_8))) { + result.add( + new String(entry.getKey(), UTF_8) + "=" + new String(entry.getValue(), UTF_8)); + } + return result; + } + + private static int sstFileCount(File directory) { + File[] files = directory.listFiles((ignored, name) -> name.endsWith(".db")); + return files == null ? 0 : files.length; + } + private static class ManuallyTriggeredExecutor extends AbstractExecutorService { private final Queue tasks = new ArrayDeque<>(); diff --git a/paimon-common/src/test/java/org/apache/paimon/lookup/sort/db/LocalKvDbTest.java b/paimon-common/src/test/java/org/apache/paimon/lookup/sort/db/LocalKvDbTest.java index 751a209d314e..8288afc7cad4 100644 --- a/paimon-common/src/test/java/org/apache/paimon/lookup/sort/db/LocalKvDbTest.java +++ b/paimon-common/src/test/java/org/apache/paimon/lookup/sort/db/LocalKvDbTest.java @@ -27,11 +27,15 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import javax.annotation.Nullable; + import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -129,6 +133,131 @@ public void testDeleteAndReinsert() throws IOException { } } + @Test + public void testRangeScanMergesMemTableAndLevels() throws IOException { + try (LocalKvDb db = createDb("range-scan-db")) { + List> initial = new ArrayList<>(); + initial.add(entry("a-1", "old-1")); + initial.add(entry("a-2", "old-2")); + initial.add(entry("b-1", "value-b")); + db.bulkLoad(initial.iterator(), initial.size()); + + putString(db, "a-1", "new-1"); + db.flush(); + deleteString(db, "a-2"); + putString(db, "a-3", "new-3"); + + Assertions.assertEquals( + Arrays.asList("a-1=new-1", "a-3=new-3"), scanStrings(db, "a", "b")); + Assertions.assertTrue(db.rangeScan("b".getBytes(UTF_8), "b".getBytes(UTF_8)).isEmpty()); + Assertions.assertEquals( + Collections.singletonList("b-1=value-b"), scanStrings(db, "b", null)); + + db.flush(); + Assertions.assertEquals( + Arrays.asList("a-1=new-1", "a-3=new-3"), scanStrings(db, "a", "b")); + db.compact(); + Assertions.assertEquals( + Arrays.asList("a-1=new-1", "a-3=new-3"), scanStrings(db, "a", "b")); + } + } + + @Test + public void testForEachInRangeAcrossMemTableAndSingleSst() throws IOException { + try (LocalKvDb db = createDb("range-consumer-db")) { + putString(db, "a-1", "value-1"); + putString(db, "a-2", "value-2"); + putString(db, "b-1", "value-b"); + + Assertions.assertEquals( + Arrays.asList("a-1=value-1", "a-2=value-2"), forEachStrings(db, "a", "b")); + + db.flush(); + Assertions.assertEquals( + Arrays.asList("a-1=value-1", "a-2=value-2"), forEachStrings(db, "a", "b")); + + putString(db, "a-1", "updated-1"); + deleteString(db, "a-2"); + putString(db, "a-3", "value-3"); + putString(db, "c-1", "value-c"); + Assertions.assertEquals( + Arrays.asList("a-1=updated-1", "a-3=value-3"), forEachStrings(db, "a", "b")); + Assertions.assertEquals( + Collections.singletonList("c-1=value-c"), forEachStrings(db, "c", "d")); + } + } + + @Test + public void testRangeIteratorDeduplicatesVersionsAcrossAllSources() throws IOException { + try (LocalKvDb db = createDb("range-iterator-db")) { + db.bulkLoad( + Arrays.asList(entry("a", "base-a"), entry("b", "base-b"), entry("c", "base-c")) + .iterator(), + 3); + + putString(db, "a", "level-zero-a-1"); + deleteString(db, "b"); + putString(db, "d", "level-zero-d"); + db.flush(); + + putString(db, "a", "level-zero-a-2"); + deleteString(db, "c"); + putString(db, "e", "level-zero-e"); + db.flush(); + + putString(db, "a", "memtable-a"); + deleteString(db, "d"); + putString(db, "f", "memtable-f"); + + List result = new ArrayList<>(); + try (LocalKvDb.RangeIterator iterator = + db.rangeIterator("a".getBytes(UTF_8), "g".getBytes(UTF_8))) { + Assertions.assertThrows( + IllegalStateException.class, () -> putString(db, "x", "blocked")); + while (iterator.advanceNext()) { + result.add( + new String(iterator.getKey().copyBytes(), UTF_8) + + "=" + + new String(iterator.getValue().copyBytes(), UTF_8)); + } + } + + Assertions.assertEquals( + Arrays.asList("a=memtable-a", "e=level-zero-e", "f=memtable-f"), result); + } + } + + @Test + public void testRangeIteratorCrossesSstBlocks() throws IOException { + File directory = new File(tempDir.toFile(), "range-iterator-blocks"); + try (LocalKvDb db = + LocalKvDb.builder(directory) + .memTableFlushThreshold(1024 * 1024) + .blockSize(128) + .level0FileNumCompactTrigger(100) + .compressOptions(new CompressOptions("none", 1)) + .build()) { + List> initial = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + initial.add(entry(String.format("key-%05d", i), String.format("value-%05d", i))); + } + db.bulkLoad(initial.iterator(), initial.size()); + + putString(db, "key-00020", "updated-20"); + deleteString(db, "key-00030"); + db.flush(); + putString(db, "key-00040", "updated-40"); + + List result = scanStrings(db, "key-00010", "key-00050"); + Assertions.assertEquals(39, result.size()); + Assertions.assertEquals("key-00010=value-00010", result.get(0)); + Assertions.assertEquals("key-00020=updated-20", result.get(10)); + Assertions.assertEquals("key-00031=value-00031", result.get(20)); + Assertions.assertEquals("key-00040=updated-40", result.get(29)); + Assertions.assertEquals("key-00049=value-00049", result.get(38)); + } + } + @Test public void testFlushToSst() throws IOException { try (LocalKvDb db = createDb()) { @@ -1726,6 +1855,31 @@ private static String getString(LocalKvDb db, String key) throws IOException { return new String(bytes, UTF_8); } + private static List scanStrings(LocalKvDb db, String from, @Nullable String to) + throws IOException { + List result = new ArrayList<>(); + for (Map.Entry entry : + db.rangeScan(from.getBytes(UTF_8), to == null ? null : to.getBytes(UTF_8))) { + result.add( + new String(entry.getKey(), UTF_8) + "=" + new String(entry.getValue(), UTF_8)); + } + return result; + } + + private static List forEachStrings(LocalKvDb db, String from, @Nullable String to) + throws IOException { + List result = new ArrayList<>(); + db.forEachInRange( + from.getBytes(UTF_8), + to == null ? null : to.getBytes(UTF_8), + (key, value) -> + result.add( + new String(key.copyBytes(), UTF_8) + + "=" + + new String(value.copyBytes(), UTF_8))); + return result; + } + private static void deleteString(LocalKvDb db, String key) throws IOException { db.delete(key.getBytes(UTF_8)); }