Skip to content
12 changes: 12 additions & 0 deletions src/Nethermind/Nethermind.Core/Extensions/DisposableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ public static void TryDispose<T1, T2>(this in (T1, T2) item) where T1 : IDisposa
if (item.Item2 is IDisposable d2) d2.Dispose();
}

/// <summary>
/// Disposes the value only when <paramref name="lazy"/> was initialized.
/// </summary>
/// <remarks>
/// Unsafe if <paramref name="lazy"/> can still be used after this call starts.
/// </remarks>
public static void DisposeIfCreated<T>(this Lazy<T> lazy) where T : IDisposable
{
if (lazy.IsValueCreated)
lazy.Value.Dispose();
}

public static void DisposeItems<T>(this IEnumerable<T> items) where T : IDisposable
{
foreach (T disposable in items)
Expand Down
10 changes: 7 additions & 3 deletions src/Nethermind/Nethermind.Db.Rocks/ColumnDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class ColumnDb : IDb, ISortedKeyValueStore, IMergeableKeyValueStore, IKey
internal readonly DbOnTheRocks _mainDb;
internal readonly ColumnFamilyHandle _columnFamily;

private readonly DbOnTheRocks.IteratorManager _iteratorManager;
private readonly DisposableLazy<DbOnTheRocks.IteratorManager>? _iteratorManager;
private readonly RocksDbReader _reader;

public ColumnDb(RocksDb rocksDb, DbOnTheRocks mainDb, string name)
Expand All @@ -30,11 +30,15 @@ public ColumnDb(RocksDb rocksDb, DbOnTheRocks mainDb, string name)
_columnFamily = _rocksDb.GetColumnFamily(name);
Name = name;

_iteratorManager = new DbOnTheRocks.IteratorManager(_rocksDb, _columnFamily, _mainDb._readAheadReadOptions);
_iteratorManager = _mainDb.CreateLazyReadAheadIteratorManager(_columnFamily);
_reader = new RocksDbReader(mainDb, mainDb.CreateReadOptions, _iteratorManager, _columnFamily);
}

public void Dispose() => _iteratorManager.Dispose();
public void Dispose()
{
_reader.Dispose();
_iteratorManager?.Dispose();
}
public string Name { get; }
Comment thread
alexb5dh marked this conversation as resolved.

byte[]? IReadOnlyKeyValueStore.Get(ReadOnlySpan<byte> key, ReadFlags flags) => _reader.Get(key, flags);
Expand Down
10 changes: 10 additions & 0 deletions src/Nethermind/Nethermind.Db.Rocks/ColumnsDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ private ColumnsDb(string basePath, DbSettings settings, IDbConfig dbConfig, IRoc
}
}

protected override void ReleaseUnmanagedResources()
{
foreach (KeyValuePair<T, ColumnDb> column in _columnDbs)
{
column.Value.Dispose();
}

base.ReleaseUnmanagedResources();
}

protected override long FetchTotalPropertyValue(string propertyName)
{
long total = 0;
Expand Down
64 changes: 47 additions & 17 deletions src/Nethermind/Nethermind.Db.Rocks/DbOnTheRocks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public partial class DbOnTheRocks : IDb, ITunableDb, IReadOnlyNativeKeyValueStor

private ReadOptions _defaultReadOptions = null!;
private ReadOptions _hintCacheMissOptions = null!;
internal ReadOptions? _readAheadReadOptions = null;
private ReadOptions? _readAheadReadOptions;

internal DbOptions? DbOptions { get; private set; }
private readonly IRocksDbConfigFactory _rocksDbConfigFactory;
Expand Down Expand Up @@ -96,7 +96,7 @@ public partial class DbOnTheRocks : IDb, ITunableDb, IReadOnlyNativeKeyValueStor
private CacheLinePaddedLong _totalReads;
private CacheLinePaddedLong _totalWrites;

private readonly IteratorManager _iteratorManager;
private readonly DisposableLazy<IteratorManager>? _iteratorManager;
private ulong _writeBufferSize;
private int _maxWriteBufferNumber;
private readonly RocksDbReader _reader;
Expand All @@ -122,7 +122,7 @@ public DbOnTheRocks(
_rocksDbConfigFactory = rocksDbConfigFactory;
_perTableDbConfig = rocksDbConfigFactory.GetForDatabase(Name, null);
_db = Init(basePath, dbSettings.DbPath, dbConfig, logManager, columnFamilies, dbSettings.DeleteOnStart, sharedCache);
_iteratorManager = new IteratorManager(_db, null, _readAheadReadOptions);
_iteratorManager = CreateLazyReadAheadIteratorManager(null);

_reader = new RocksDbReader(this, CreateReadOptions, _iteratorManager, null);

Expand Down Expand Up @@ -749,6 +749,8 @@ internal ReadOptions CreateReadOptions()

internal byte[]? GetWithIterator(ReadOnlySpan<byte> key, ColumnFamilyHandle? cf, IteratorManager iteratorManager, ReadFlags flags, out bool success)
{
ThrowIfDisposing();
Comment thread
alexb5dh marked this conversation as resolved.

success = true;

using IteratorManager.RentWrapper wrapper = iteratorManager.Rent(flags);
Expand All @@ -769,6 +771,16 @@ internal ReadOptions CreateReadOptions()
return null;
}

/// <summary>
/// Pool for calls with <see cref="ReadFlags.HintReadAhead"/> - tailing iterators with large read steps.
/// Null when read-ahead is turned off.
/// </summary>
internal DisposableLazy<IteratorManager>? CreateLazyReadAheadIteratorManager(ColumnFamilyHandle? cf) =>
_readAheadReadOptions is null ? null : CreateLazyIteratorManager(cf, _readAheadReadOptions);

private DisposableLazy<IteratorManager> CreateLazyIteratorManager(ColumnFamilyHandle? cf, ReadOptions readOptions) =>
Comment thread
alexb5dh marked this conversation as resolved.
Outdated
new(() => new IteratorManager(_db, cf, readOptions));

internal unsafe byte[]? Get(ReadOnlySpan<byte> key, ColumnFamilyHandle? cf, ReadOptions readOptions)
{
// TODO: update when merged upstream: https://github.com/curiosity-ai/rocksdb-sharp/pull/61
Expand Down Expand Up @@ -1572,7 +1584,7 @@ private class FlushOptions
}
}

private void ReleaseUnmanagedResources()
protected virtual void ReleaseUnmanagedResources()
{
// ReSharper disable once ConstantConditionalAccessQualifier
// running in finalizer, potentially not fully constructed
Expand All @@ -1581,7 +1593,7 @@ private void ReleaseUnmanagedResources()
batch.Dispose();
}

_iteratorManager.Dispose();
_iteratorManager?.Dispose();
_db.Dispose();

if (_rowCache.HasValue)
Expand Down Expand Up @@ -1857,14 +1869,17 @@ public class IteratorManager : IDisposable
private readonly ManagedIterators _readaheadIterators3 = new();
private readonly RocksDb _rocksDb;
private readonly ColumnFamilyHandle? _cf;
private readonly ReadOptions? _readOptions;
private readonly ReadOptions _readOptions;
private readonly Timer _timer;

// used to guarantee iterators in timer are not accessed after DB disposal
private readonly Lock _disposeLock = new();
private bool _isDisposed;

// This is about once every two second maybe at max throughput.
private const int IteratorUsageLimit = 1000000;

public IteratorManager(RocksDb rocksDb, ColumnFamilyHandle? cf, ReadOptions? readOptions)
public IteratorManager(RocksDb rocksDb, ColumnFamilyHandle? cf, ReadOptions readOptions)
{
_rocksDb = rocksDb;
_cf = cf;
Expand All @@ -1875,20 +1890,35 @@ public IteratorManager(RocksDb rocksDb, ColumnFamilyHandle? cf, ReadOptions? rea

private void OnTimer(object? state)
{
if (_isDisposed) return;
_readaheadIterators.ClearIterators();
_readaheadIterators2.ClearIterators();
_readaheadIterators3.ClearIterators();
// Skip the tick instead of stacking up callbacks
if (!_disposeLock.TryEnter()) return;

try
{
if (_isDisposed) return;

_readaheadIterators.ClearIterators();
_readaheadIterators2.ClearIterators();
_readaheadIterators3.ClearIterators();
}
finally
{
_disposeLock.Exit();
}
}

public void Dispose()
{
if (_isDisposed) return;
_isDisposed = true;
_timer.Dispose();
_readaheadIterators.DisposeAll();
_readaheadIterators2.DisposeAll();
_readaheadIterators3.DisposeAll();
lock (_disposeLock)
{
if (_isDisposed) return;
_isDisposed = true;

_timer.Dispose();
_readaheadIterators.DisposeAll();
_readaheadIterators2.DisposeAll();
_readaheadIterators3.DisposeAll();
}
}

public RentWrapper Rent(ReadFlags flags)
Expand Down
47 changes: 47 additions & 0 deletions src/Nethermind/Nethermind.Db.Rocks/DisposableLazy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;
using System.Threading;
using Nethermind.Core.Extensions;

namespace Nethermind.Db.Rocks;

/// <summary>
/// A <see cref="Lazy{T}"/> for an <see cref="IDisposable"/> type that never leaks an undisposed value.
/// </summary>
/// <remarks>
/// Reading an already created value skips the lock, so a value created before disposal is still handed out after it.
/// Callers that must reject use after disposal need their own check.
/// </remarks>
public sealed class DisposableLazy<T>(Func<T> factory) : IDisposable where T : class, IDisposable
{
private readonly Lazy<T> _lazy = new(factory, LazyThreadSafetyMode.ExecutionAndPublication);
private readonly Lock _lock = new();
private bool _disposed;
Comment thread
alexb5dh marked this conversation as resolved.

/// <exception cref="ObjectDisposedException">The value was not created before this instance was disposed.</exception>
public T Value
{
get
{
if (_lazy.IsValueCreated) return _lazy.Value;

lock (_lock)
{
ObjectDisposedException.ThrowIf(_disposed, this);
return _lazy.Value;
}
}
Comment thread
alexb5dh marked this conversation as resolved.
}

public void Dispose()
{
lock (_lock)
{
if (_disposed) return;
_disposed = true;
_lazy.DisposeIfCreated();
}
}
}
8 changes: 4 additions & 4 deletions src/Nethermind/Nethermind.Db.Rocks/RocksDbReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ public class RocksDbReader(DbOnTheRocks mainDb,
ReadOptions options,
ReadOptions hintCacheMissOptions,
Func<ReadOptions> readOptionsFactory,
DbOnTheRocks.IteratorManager? iteratorManager = null,
DisposableLazy<DbOnTheRocks.IteratorManager>? iteratorManager = null,
ColumnFamilyHandle? columnFamily = null) : ISortedKeyValueStore, IDisposable
{
private readonly DbOnTheRocks _mainDb = mainDb;
private readonly Func<ReadOptions> _readOptionsFactory = readOptionsFactory;
private readonly DbOnTheRocks.IteratorManager? _iteratorManager = iteratorManager;
private readonly DisposableLazy<DbOnTheRocks.IteratorManager>? _iteratorManager = iteratorManager;
private readonly ColumnFamilyHandle? _columnFamily = columnFamily;

private readonly ReadOptions _options = options;
Expand All @@ -40,7 +40,7 @@ public class RocksDbReader(DbOnTheRocks mainDb,

public RocksDbReader(DbOnTheRocks mainDb,
Func<ReadOptions> readOptionsFactory,
DbOnTheRocks.IteratorManager? iteratorManager = null,
DisposableLazy<DbOnTheRocks.IteratorManager>? iteratorManager = null,
ColumnFamilyHandle? columnFamily = null)
: this(mainDb, readOptionsFactory(), readOptionsFactory(), readOptionsFactory, iteratorManager, columnFamily)
{
Expand Down Expand Up @@ -73,7 +73,7 @@ internal static void DestroyReadOptions(ReadOptions options)
{
if ((flags & ReadFlags.HintReadAhead) != 0 && _iteratorManager is not null)
{
byte[]? result = _mainDb.GetWithIterator(key, _columnFamily, _iteratorManager, flags, out bool success);
byte[]? result = _mainDb.GetWithIterator(key, _columnFamily, _iteratorManager.Value, flags, out bool success);
if (success)
{
return result;
Expand Down
Loading