From 9da05140a55dd424815d33e8c030fb18561b0abf Mon Sep 17 00:00:00 2001 From: Jimmy Date: Sun, 21 Jun 2026 07:22:53 +0800 Subject: [PATCH] Fix snapshot point read cache options --- .../LevelDBStore/Plugins/Storage/Snapshot.cs | 19 ++-- .../RocksDBStore/Plugins/Storage/Snapshot.cs | 20 ++-- .../SnapshotReadOptionsTest.cs | 102 ++++++++++++++++++ 3 files changed, 125 insertions(+), 16 deletions(-) create mode 100644 tests/Neo.Plugins.Storage.Tests/SnapshotReadOptionsTest.cs diff --git a/plugins/LevelDBStore/Plugins/Storage/Snapshot.cs b/plugins/LevelDBStore/Plugins/Storage/Snapshot.cs index 6d31165ba..5edff4002 100644 --- a/plugins/LevelDBStore/Plugins/Storage/Snapshot.cs +++ b/plugins/LevelDBStore/Plugins/Storage/Snapshot.cs @@ -25,7 +25,8 @@ internal class Snapshot : IStoreSnapshot, IEnumerable public IEnumerable<(byte[] Key, byte[] Value)> Find(byte[]? keyOrPrefix, SeekDirection direction = SeekDirection.Forward) { - return _db.Seek(_readOptions, keyOrPrefix, direction); + return _db.Seek(_scanReadOptions, keyOrPrefix, direction); } public bool Contains(byte[] key) { - return _db.Contains(_readOptions, key); + return _db.Contains(_pointReadOptions, key); } public byte[]? TryGet(byte[] key) { - return _db.Get(_readOptions, key); + return _db.Get(_pointReadOptions, key); } public bool TryGet(byte[] key, [NotNullWhen(true)] out byte[]? value) { - value = _db.Get(_readOptions, key); + value = _db.Get(_pointReadOptions, key); return value != null; } public IEnumerator> GetEnumerator() { - using var iterator = _db.CreateIterator(_readOptions); + using var iterator = _db.CreateIterator(_scanReadOptions); for (iterator.SeekToFirst(); iterator.Valid(); iterator.Next()) yield return new KeyValuePair(iterator.Key()!, iterator.Value()!); } diff --git a/plugins/RocksDBStore/Plugins/Storage/Snapshot.cs b/plugins/RocksDBStore/Plugins/Storage/Snapshot.cs index 9c3fc170e..cdfa5deb5 100644 --- a/plugins/RocksDBStore/Plugins/Storage/Snapshot.cs +++ b/plugins/RocksDBStore/Plugins/Storage/Snapshot.cs @@ -23,7 +23,8 @@ internal class Snapshot : IStoreSnapshot private readonly RocksDb _db; private readonly RocksDbSharp.Snapshot _snapshot; private readonly WriteBatch _batch; - private readonly ReadOptions _options; + private readonly ReadOptions _scanOptions; + private readonly ReadOptions _pointOptions; private readonly Lock _lock = new(); public IStore Store { get; } @@ -35,9 +36,12 @@ internal Snapshot(Store store, RocksDb db) _snapshot = db.CreateSnapshot(); _batch = new WriteBatch(); - _options = new ReadOptions(); - _options.SetFillCache(false); - _options.SetSnapshot(_snapshot); + _scanOptions = new ReadOptions(); + _scanOptions.SetFillCache(false); + _scanOptions.SetSnapshot(_snapshot); + + _pointOptions = new ReadOptions(); + _pointOptions.SetSnapshot(_snapshot); } public void Commit() @@ -63,7 +67,7 @@ public void Put(byte[] key, byte[] value) { keyOrPrefix ??= []; - using var it = _db.NewIterator(readOptions: _options); + using var it = _db.NewIterator(readOptions: _scanOptions); if (direction == SeekDirection.Forward) for (it.Seek(keyOrPrefix); it.Valid(); it.Next()) @@ -75,17 +79,17 @@ public void Put(byte[] key, byte[] value) public bool Contains(byte[] key) { - return _db.Get(key, Array.Empty(), 0, 0, readOptions: _options) >= 0; + return _db.Get(key, Array.Empty(), 0, 0, readOptions: _pointOptions) >= 0; } public byte[]? TryGet(byte[] key) { - return _db.Get(key, readOptions: _options); + return _db.Get(key, readOptions: _pointOptions); } public bool TryGet(byte[] key, [NotNullWhen(true)] out byte[]? value) { - value = _db.Get(key, readOptions: _options); + value = _db.Get(key, readOptions: _pointOptions); return value != null; } diff --git a/tests/Neo.Plugins.Storage.Tests/SnapshotReadOptionsTest.cs b/tests/Neo.Plugins.Storage.Tests/SnapshotReadOptionsTest.cs new file mode 100644 index 000000000..564c9031c --- /dev/null +++ b/tests/Neo.Plugins.Storage.Tests/SnapshotReadOptionsTest.cs @@ -0,0 +1,102 @@ +// Copyright (C) 2015-2026 The Neo Project. +// +// SnapshotReadOptionsTest.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Reflection; + +namespace Neo.Plugins.Storage.Tests; + +[TestClass] +public class SnapshotReadOptionsTest +{ + [TestMethod] + public void LevelDbSnapshotUsesDedicatedReadOptionsForScanAndPointReads() + { + var snapshotType = GetSnapshotType(typeof(LevelDBStore)); + var scanReadOptions = GetField(snapshotType, "_scanReadOptions"); + var pointReadOptions = GetField(snapshotType, "_pointReadOptions"); + + AssertMethodUsesOnly(snapshotType, "Find", scanReadOptions, pointReadOptions); + AssertMethodUsesOnly(snapshotType, "GetEnumerator", scanReadOptions, pointReadOptions); + AssertMethodUsesOnly(snapshotType, "Contains", pointReadOptions, scanReadOptions); + AssertMethodUsesOnly(snapshotType, "TryGet", pointReadOptions, scanReadOptions); + } + + [TestMethod] + public void RocksDbSnapshotUsesDedicatedReadOptionsForScanAndPointReads() + { + var snapshotType = GetSnapshotType(typeof(RocksDBStore)); + var scanOptions = GetField(snapshotType, "_scanOptions"); + var pointOptions = GetField(snapshotType, "_pointOptions"); + + AssertMethodUsesOnly(snapshotType, "Find", scanOptions, pointOptions); + AssertMethodUsesOnly(snapshotType, "Contains", pointOptions, scanOptions); + AssertMethodUsesOnly(snapshotType, "TryGet", pointOptions, scanOptions); + } + + private static Type GetSnapshotType(Type storeProviderType) + { + return storeProviderType.Assembly.GetType("Neo.Plugins.Storage.Snapshot", throwOnError: true)!; + } + + private static FieldInfo GetField(Type type, string fieldName) + { + var field = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsNotNull(field, $"{type.FullName} should declare {fieldName}."); + return field; + } + + private static void AssertMethodUsesOnly(Type type, string methodName, FieldInfo expectedField, FieldInfo unexpectedField) + { + var implementations = GetImplementations(type, methodName).ToArray(); + Assert.IsNotEmpty(implementations, $"{type.FullName}.{methodName} should exist."); + Assert.IsTrue( + implementations.Any(method => UsesField(method, expectedField)), + $"{type.FullName}.{methodName} should use {expectedField.Name}."); + Assert.IsFalse( + implementations.Any(method => UsesField(method, unexpectedField)), + $"{type.FullName}.{methodName} should not use {unexpectedField.Name}."); + } + + private static IEnumerable GetImplementations(Type type, string methodName) + { + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + foreach (var method in type.GetMethods(flags).Where(method => method.Name == methodName)) + yield return method; + + foreach (var nestedType in type.GetNestedTypes(BindingFlags.NonPublic)) + { + if (!nestedType.Name.Contains($"<{methodName}>")) continue; + + var moveNext = nestedType.GetMethod("MoveNext", flags); + if (moveNext != null) + yield return moveNext; + } + } + + private static bool UsesField(MethodBase method, FieldInfo field) + { + var body = method.GetMethodBody()?.GetILAsByteArray(); + if (body is null) return false; + + var token = BitConverter.GetBytes(field.MetadataToken); + for (var i = 0; i <= body.Length - token.Length; i++) + { + if (body[i] == token[0] + && body[i + 1] == token[1] + && body[i + 2] == token[2] + && body[i + 3] == token[3]) + return true; + } + + return false; + } +}