From 39d8f948c347c7cae6e584c4286f1e1c640b9073 Mon Sep 17 00:00:00 2001 From: Havret Date: Thu, 8 Jan 2026 13:20:21 +0100 Subject: [PATCH] Add ColumnFamilyOptions management to prevent garbage collection of merge operator delegates --- src/RocksDb.Extensions/RocksDbContext.cs | 32 +++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/RocksDb.Extensions/RocksDbContext.cs b/src/RocksDb.Extensions/RocksDbContext.cs index 4ba20ca..fb2c14e 100644 --- a/src/RocksDb.Extensions/RocksDbContext.cs +++ b/src/RocksDb.Extensions/RocksDbContext.cs @@ -8,6 +8,16 @@ internal class RocksDbContext : IDisposable private readonly WriteOptions _writeOptions; private readonly Dictionary _mergeOperators; private readonly Cache _cache; + + // ReSharper disable once CollectionNeverQueried.Local + /// + /// Stores ColumnFamilyOptions instances to prevent garbage collection of merge operator delegates. + /// This is critical because ColumnFamilyOptions holds MergeOperatorRef which contains the delegates + /// (FullMerge and PartialMerge) that RocksDB (native code) references. Without keeping the + /// ColumnFamilyOptions alive, the GC may collect MergeOperatorRef and its delegates, causing the error: + /// "A callback was made on a garbage collected delegate of type 'RocksDbSharp!RocksDbSharp.GetMergeOperator::Invoke'" + /// + private readonly Dictionary _columnFamilyOptions = new(); private const long BlockCacheSize = 50 * 1024 * 1024L; private const long BlockSize = 4096L; @@ -38,21 +48,21 @@ public RocksDbContext(IOptions options) dbOptions.SetWriteBufferSize(WriteBufferSize); dbOptions.SetCompression(Compression.No); dbOptions.SetCompactionStyle(Compaction.Universal); - + var tableConfig = new BlockBasedTableOptions(); tableConfig.SetBlockCache(_cache); tableConfig.SetBlockSize(BlockSize); - + var filter = BloomFilterPolicy.Create(); tableConfig.SetFilterPolicy(filter); - + dbOptions.SetBlockBasedTableFactory(tableConfig); - + _writeOptions = new WriteOptions(); _writeOptions.DisableWal(1); _mergeOperators = options.Value.MergeOperators; - + var columnFamilies = CreateColumnFamilies(options.Value.ColumnFamilies); if (options.Value.DeleteExistingDatabaseOnStartup) @@ -75,6 +85,10 @@ private static void DestroyDatabase(string path) public ColumnFamilyOptions CreateColumnFamilyOptions(string columnFamilyName) { + // Remove old options if they exist (e.g., when recreating column family in Clear()) + // We don't need to dispose them - just remove the reference and let GC handle it + _columnFamilyOptions.Remove(columnFamilyName); + var cfOptions = new ColumnFamilyOptions(); if (_mergeOperators.TryGetValue(columnFamilyName, out var mergeOperatorConfig)) { @@ -86,6 +100,9 @@ public ColumnFamilyOptions CreateColumnFamilyOptions(string columnFamilyName) cfOptions.SetMergeOperator(mergeOp); } + // Store the options to keep MergeOperatorRef (and its delegates) alive + _columnFamilyOptions[columnFamilyName] = cfOptions; + return cfOptions; } @@ -104,6 +121,9 @@ private ColumnFamilies CreateColumnFamilies(IReadOnlyList columnFamilyNa public void Dispose() { + // Clear column family options to allow garbage collection + _columnFamilyOptions.Clear(); + Db.Dispose(); } -} +} \ No newline at end of file