Skip to content
Merged
Show file tree
Hide file tree
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
10 changes: 6 additions & 4 deletions libs/server/Transaction/TransactionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -554,11 +554,13 @@ void ComputeSublogAccessVector(out ulong physicalSublogAccessVector, out BitVect
// Initialize only for multi-log
virtualSublogAccessVector = [.. Enumerable.Range(0, appendOnlyFile.Log.Size).Select(_ => new BitVector(AofShardedLogTransactionHeader.ReplayTaskAccessVectorBytes))];

// If sharded log is enabled calculate sublog access bitmap
for (var i = 0; i < txnKeysParseState.Count; i++)
// Compute the sublog access bitmap from the transaction's locked keys. keyEntries is populated in both
// standalone and cluster mode (via SaveKeyEntryToLock), unlike txnKeysParseState which is only filled during
// cluster slot verification (TxnClusterSlotCheck.SaveKeyArgSlice returns early when !clusterEnabled). The
// stored keyHash equals GarnetLog.HASH of the key, so it is used directly (no re-hashing).
for (var i = 0; i < keyEntries.Count; i++)
{
ref var key = ref txnKeysParseState.GetArgSliceByRef(i);
var keyHash = GarnetLog.HASH(key.ReadOnlySpan);
var keyHash = keyEntries.GetKeyHash(i);
var physicalSublogIdx = appendOnlyFile.Log.GetPhysicalSublogIdx(keyHash);
var replayIdx = appendOnlyFile.Log.GetReplayTaskIdx(keyHash);
physicalSublogAccessVector |= 1UL << physicalSublogIdx;
Expand Down
9 changes: 9 additions & 0 deletions libs/server/Transaction/TxnKeyEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ public bool IsReadOnly
}
}

/// <summary>Number of keys registered for locking in this transaction.</summary>
internal int Count => keyCount;

/// <summary>
/// Hash of the key at <paramref name="index"/>. This is the Garnet key hash (equal to <c>GarnetLog.HASH</c> of the
/// key bytes), so it can be used directly for AOF sublog routing.
/// </summary>
internal long GetKeyHash(int index) => keys[index].keyHash;

public void AddKey(PinnedSpanByte keyArgSlice, LockType type)
{
var keyHash = comparison.UnifiedTransactionalContext.GetKeyHash((FixedSpanByteKey)keyArgSlice);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

<ItemGroup>
<ProjectReference Include="../../../libs/native/bftree-garnet/BfTreeInterop.csproj" />
<ProjectReference Include="../Garnet.test/Garnet.test.csproj" />
</ItemGroup>

</Project>
15 changes: 15 additions & 0 deletions test/standalone/BfTreeInterop.test/TestProjectSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using Garnet.test;
using NUnit.Framework;

namespace BfTreeInterop.test
{
[SetUpFixture]
public class TestProjectSetup
{
[OneTimeSetUp]
public void SetPort() => TestUtils.SetTestPort(TestPortAssignment.GarnetTestBfTreeInterop);
}
}
95 changes: 95 additions & 0 deletions test/standalone/Garnet.test/AofShardedTxnRecoveryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using NUnit.Framework;
using NUnit.Framework.Legacy;
using StackExchange.Redis;

namespace Garnet.test
{
/// <summary>
/// Verifies that an internal (implicit) transaction survives an AOF recovery restart on a standalone server with
/// multiple physical AOF sublogs (AofPhysicalSublogCount &gt; 1). RENAME is used as the transaction: it must route its
/// TxnStart marker to the physical sublog(s) its keys hash to, which ComputeSublogAccessVector derives from the
/// transaction's locked keys. Parameterized over subLogCount: 1 (single physical log) and 2 (sharded).
/// </summary>
[TestFixture]
public class AofShardedTxnRecoveryTests : TestBase
{
GarnetServer server;

GarnetServer CreateShardedAofServer(int subLogCount, bool tryRecover)
{
// Standalone (enableCluster: false) server with subLogCount physical AOF sublogs, built via
// GetGarnetServerOptions (the standalone CreateGarnetServer overload does not expose sublogCount).
var opts = TestUtils.GetGarnetServerOptions(
checkpointDir: TestUtils.MethodTestDir,
logDir: TestUtils.MethodTestDir,
endpoint: TestUtils.EndPoint,
enableCluster: false,
enableAOF: true,
tryRecover: tryRecover,
sublogCount: subLogCount);
return new GarnetServer(opts);
}

[SetUp]
public void Setup()
{
TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true);

// The test method carries a [Values(1, 2)] int subLogCount, but [SetUp] cannot receive the test case's
// arguments directly, so read subLogCount from the current test's arguments (see Tsavorite's
// OverflowBucketLockTableTests.Setup).
var subLogCount = 1;
foreach (var arg in TestContext.CurrentContext.Test.Arguments)
{
if (arg is int sc)
{
subLogCount = sc;
break;
}
}

server = CreateShardedAofServer(subLogCount, tryRecover: false);
server.Start();
}

[TearDown]
public void TearDown()
{
server?.Dispose();
TestUtils.OnTearDown();
}

[Test]
public void ShardedStandaloneRenameTransactionSurvivesRecovery([Values(1, 2)] int subLogCount)
{
using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()))
{
var db = redis.GetDatabase(0);
db.StringSet("k1", "v1");

// RENAME runs as an internal (implicit) transaction: its TxnStart marker must reach the physical
// sublog(s) its keys hash to. This exercises the standalone sharded transaction path.
ClassicAssert.IsTrue(db.KeyRename("k1", "k2"));
ClassicAssert.AreEqual("v1", (string)db.StringGet("k2"));

// Commit the AOF to disk so the restart below must replay the RENAME transaction.
db.Execute("COMMITAOF");
}

// Restart with recovery (keep the on-disk AOF/checkpoint).
server.Dispose(false);
server = CreateShardedAofServer(subLogCount, tryRecover: true);
server.Start();

using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()))
{
var db = redis.GetDatabase(0);
ClassicAssert.AreEqual("v1", (string)db.StringGet("k2"), "renamed key should survive recovery");
ClassicAssert.IsFalse(db.KeyExists("k1"), "old key should not exist after rename + recovery");
}
}
}
}
1 change: 1 addition & 0 deletions test/standalone/Garnet.test/Garnet.test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<InternalsVisibleTo Include="Garnet.test.vectorset" Key="0024000004800000940000000602000000240000525341310004000001000100011b1661238d3d3c76232193c8aa2de8c05b8930d6dfe8cd88797a8f5624fdf14a1643141f31da05c0f67961b0e3a64c7120001d2f8579f01ac788b0ff545790d44854abe02f42bfe36a056166a75c6a694db8c5b6609cff8a2dbb429855a1d9f79d4d8ec3e145c74bfdd903274b7344beea93eab86b422652f8dd8eecf530d2" />
<InternalsVisibleTo Include="Garnet.test.rangeindex" Key="0024000004800000940000000602000000240000525341310004000001000100011b1661238d3d3c76232193c8aa2de8c05b8930d6dfe8cd88797a8f5624fdf14a1643141f31da05c0f67961b0e3a64c7120001d2f8579f01ac788b0ff545790d44854abe02f42bfe36a056166a75c6a694db8c5b6609cff8a2dbb429855a1d9f79d4d8ec3e145c74bfdd903274b7344beea93eab86b422652f8dd8eecf530d2" />
<InternalsVisibleTo Include="Garnet.test.extensions" Key="0024000004800000940000000602000000240000525341310004000001000100011b1661238d3d3c76232193c8aa2de8c05b8930d6dfe8cd88797a8f5624fdf14a1643141f31da05c0f67961b0e3a64c7120001d2f8579f01ac788b0ff545790d44854abe02f42bfe36a056166a75c6a694db8c5b6609cff8a2dbb429855a1d9f79d4d8ec3e145c74bfdd903274b7344beea93eab86b422652f8dd8eecf530d2" />
<InternalsVisibleTo Include="BfTreeInterop.test" Key="0024000004800000940000000602000000240000525341310004000001000100011b1661238d3d3c76232193c8aa2de8c05b8930d6dfe8cd88797a8f5624fdf14a1643141f31da05c0f67961b0e3a64c7120001d2f8579f01ac788b0ff545790d44854abe02f42bfe36a056166a75c6a694db8c5b6609cff8a2dbb429855a1d9f79d4d8ec3e145c74bfdd903274b7344beea93eab86b422652f8dd8eecf530d2" />
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions test/standalone/Garnet.test/TestUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public enum TestPortAssignment
GarnetTestRangeIndex = 34700,
GarnetTestScripting = 34800,
GarnetTestVectorSet = 34900,
GarnetTestBfTreeInterop = 35000,
}

internal static class TestUtils
Expand Down
Loading