Skip to content

[Java] RocksDB.close() never closes defaultReadOptions_, leaking a native ReadOptions per DB instance #15036

Description

@alanlau28

Summary

org.rocksdb.RocksDB allocates a ReadOptions in its constructor and never releases it:

private final ReadOptions defaultReadOptions_;   // assigned in RocksDB(long)

RocksDB.close() closes the owned column family handles, calls closeDatabase(nativeHandle_),
and disposes its own handle — but never references defaultReadOptions_. AbstractNativeReference
has no finalizer, and there is no Cleaner or phantom-reference cleanup registered anywhere in the
AbstractNativeReference / AbstractImmutableNativeReference / RocksObject hierarchy, so an
explicit Java close() is the only path to disposeInternal(). That call never comes.

Every RocksDB instance opened leaks one native ReadOptions for the lifetime of the JVM.
Garbage collecting the Java wrapper does not help.

Affected versions

Verified by decompiling released jars from Maven Central:

Version defaultReadOptions_ Released by close()
7.9.2, 8.0.0, 8.6.7, 8.9.1 absent n/a
8.10.0 present no
8.10.2, 8.11.3, 8.11.5 present no
9.0.0, 9.3.1, 9.7.3 present no
10.1.3 present no
10.10.1, 10.10.1.1 (latest) present no

Introduced in 8.10.0 — the same change that switched newIterator() from native iterator(J)
to iterator(JJJ), so an explicit column family handle and read options handle are passed through
to JNI. close() was not updated to match, and has not been since.

In 10.10.1 the field is referenced from exactly four places: its declaration, the RocksDB(long)
constructor, newIterator(), and newIterator(ColumnFamilyHandle). Nothing closes it.

Reproduction

Self-contained, no dependencies beyond rocksdbjni:

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.util.HashSet;
import java.util.Set;

import org.rocksdb.Options;
import org.rocksdb.ReadOptions;
import org.rocksdb.RocksDB;
import org.rocksdb.RocksObject;

public class ReadOptionsLeak {
    public static void main(final String[] args) throws Exception {
        RocksDB.loadLibrary();
        final Field f = RocksDB.class.getDeclaredField("defaultReadOptions_");
        f.setAccessible(true);
        final Field h = RocksObject.class.getDeclaredField("nativeHandle_");
        h.setAccessible(true);

        final String dir = Files.createTempDirectory("leak-").toString();

        // 1. after close(), the DB handle is released but its ReadOptions is not
        try (Options o = new Options().setCreateIfMissing(true)) {
            final RocksDB db = RocksDB.open(o, dir);
            final ReadOptions defaults = (ReadOptions) f.get(db);
            db.close();
            System.out.println("db owns=" + db.isOwningHandle()
                    + "  defaultReadOptions_ owns=" + defaults.isOwningHandle());
        }

        // 2. the native block is never returned to the allocator: with nothing
        //    freed, no address can ever be reused.
        for (final boolean fix : new boolean[] {false, true}) {
            final Set<Long> addrs = new HashSet<>();
            for (int i = 0; i < 2000; i++) {
                try (Options o = new Options().setCreateIfMissing(true)) {
                    final RocksDB db = RocksDB.open(o, dir);
                    final ReadOptions defaults = (ReadOptions) f.get(db);
                    addrs.add(h.getLong(defaults));
                    db.close();
                    if (fix) defaults.close();
                }
            }
            System.out.println((fix ? "with close()" : "as-is")
                    + ": " + addrs.size() + " distinct addresses / 2000 cycles");
        }
    }
}

Actual output (rocksdbjni 10.10.1, JDK 17, macOS/aarch64)

db owns=false  defaultReadOptions_ owns=true
as-is: 2000 distinct addresses / 2000 cycles
with close(): 2 distinct addresses / 2000 cycles

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions