Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions src/RocksDb.Extensions/RocksDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ internal class RocksDbContext : IDisposable
private readonly WriteOptions _writeOptions;
private readonly Dictionary<string, MergeOperatorConfig> _mergeOperators;
private readonly Cache _cache;

// ReSharper disable once CollectionNeverQueried.Local
/// <summary>
/// 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'"
/// </summary>
private readonly Dictionary<string, ColumnFamilyOptions> _columnFamilyOptions = new();

private const long BlockCacheSize = 50 * 1024 * 1024L;
private const long BlockSize = 4096L;
Expand Down Expand Up @@ -38,21 +48,21 @@ public RocksDbContext(IOptions<RocksDbOptions> 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)
Expand All @@ -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))
{
Expand All @@ -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;
}

Expand All @@ -104,6 +121,9 @@ private ColumnFamilies CreateColumnFamilies(IReadOnlyList<string> columnFamilyNa

public void Dispose()
{
// Clear column family options to allow garbage collection
_columnFamilyOptions.Clear();

Db.Dispose();
}
}
}