Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e99ed6c
add max sublog vector info replication
vazois Jul 24, 2026
8d0e276
reader side throttling
vazois Jul 27, 2026
7f8720e
call sync replay throttle
vazois Jul 31, 2026
6fd1289
add drift barrier control config to server config
vazois Jul 31, 2026
42e5611
refactor tail witness to execute inline
vazois Jul 31, 2026
1212b07
ONLY FOR TESTING
vazois Jul 31, 2026
b9c442b
add AOF shipping backpressure on primary
vazois Jul 31, 2026
8515cba
rename parameters
vazois Aug 1, 2026
87caf94
tight spin wait in BulkConsume
vazois Aug 1, 2026
fc34dbf
update max sequence number ahead of barrier check
vazois Aug 1, 2026
1df9fd9
refresh the shipped watermark in the backpressure gate outside epoch …
vazois Aug 1, 2026
35309c8
LOGGING FOR TESTING
vazois Aug 1, 2026
3b1c392
re-read tail on wait
vazois Aug 1, 2026
e186a20
improve prefetch and update of virtual sublog replay state
vazois Aug 3, 2026
5f16499
bail early on waiter release if the minTarget has not been reached
vazois Aug 4, 2026
4deaa2c
add re-usable waiter instance for Reader
vazois Aug 4, 2026
6d10652
add runtime backpressure enable/disable
vazois Aug 5, 2026
3367b87
eliminate logging for testing
vazois Aug 5, 2026
a2041f1
fix race condition
vazois Aug 5, 2026
385ea07
avoid MRE frequent allocations
vazois Aug 6, 2026
7f9d551
refactor and rename to improve clarity
vazois Aug 6, 2026
b6faadd
ensure bounded wait for ReplayAlignBarrier wait
vazois Aug 6, 2026
a42b547
wip; writer side drift check and bounding
vazois Aug 6, 2026
c4b48d4
pre-allocate MRE per virtual sublog to avoid missed update race
vazois Aug 6, 2026
5363540
enable pulse backpressure on stalling to avoid deadlock during replay…
vazois Aug 6, 2026
808d9a4
cleanup align barrier trigger check to make code more readable
vazois Aug 6, 2026
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
2 changes: 1 addition & 1 deletion benchmark/Resp.benchmark/OfflineBench/AOFBench/AofBench.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public static GarnetServerOptions GetServerOptions(Options options)
CommitFrequencyMs = options.CommitFrequencyMs,
AofPhysicalSublogCount = options.AofPhysicalSublogCount,
AofReplayTaskCount = options.AofReplayTaskCount,
ReplicationOffsetMaxLag = 0,
AofReplayMaxLagBytes = 0,
CheckpointDir = OperatingSystem.IsLinux() ? "/tmp" : null,
};
return serverOptions;
Expand Down
2 changes: 1 addition & 1 deletion benchmark/Resp.benchmark/OfflineBench/AOFBench/AofGen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ public AofGen(Options options)
CommitFrequencyMs = -1,
FastAofTruncate = true,
EnableCluster = true,
ReplicationOffsetMaxLag = 0,
AofReplayMaxLagBytes = 0,
AofPhysicalSublogCount = options.AofPhysicalSublogCount
};
aofServerOptions.GetAofSettings(0, out var logSettings);
Expand Down
4 changes: 2 additions & 2 deletions benchmark/Resp.benchmark/OfflineBench/GarnetServerInstance.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using System.Net;
Expand Down Expand Up @@ -27,7 +27,7 @@ public static GarnetServerOptions GetServerOptions(Options options)
CommitFrequencyMs = options.CommitFrequencyMs,
AofPhysicalSublogCount = options.AofPhysicalSublogCount,
AofReplayTaskCount = options.AofReplayTaskCount,
ReplicationOffsetMaxLag = 0,
AofReplayMaxLagBytes = 0,
CheckpointDir = OperatingSystem.IsLinux() ? "/tmp" : null,
};
return serverOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -548,19 +548,16 @@ public void SetClusterSyncHeader(string sourceNodeId)
}

/// <summary>
/// Issue CLUSTER ADVANCE_TIME
/// Issue an in-band CLUSTER ADVANCE_TIME pulse for one physical sublog. No response is
/// expected, so the pulse remains ordered with APPENDLOG traffic on this connection.
/// </summary>
/// <param name="physicalSublogIdx"></param>
/// <param name="sequenceNumber"></param>
/// <param name="aofAddress"></param>
/// <returns></returns>
/// <seealso cref="M:Garnet.cluster.ClusterSession.NetworkClusterAdvanceTime"/>
public Task<string> ExecuteClusterAdvanceTime(long sequenceNumber, Span<byte> aofAddress)
public void ExecuteClusterAdvanceTime(int physicalSublogIdx, long sequenceNumber)
{
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
tcsQueue.Enqueue(tcs);
var curr = offset;
var argCount = 2;
var arraySize = 2 + argCount;
const int arraySize = 4;

while (!RespWriteUtils.TryWriteArrayLength(arraySize, ref curr, end))
{
Expand All @@ -586,24 +583,20 @@ public Task<string> ExecuteClusterAdvanceTime(long sequenceNumber, Span<byte> ao
offset = curr;

//3
while (!RespWriteUtils.TryWriteArrayItem(sequenceNumber, ref curr, end))
while (!RespWriteUtils.TryWriteArrayItem(physicalSublogIdx, ref curr, end))
{
Flush();
curr = offset;
}
offset = curr;

//4
while (!RespWriteUtils.TryWriteBulkString(aofAddress, ref curr, end))
while (!RespWriteUtils.TryWriteArrayItem(sequenceNumber, ref curr, end))
{
Flush();
curr = offset;
}
offset = curr;

Flush();
Interlocked.Increment(ref numCommands);
return tcs.Task;
}

/// <summary>
Expand Down
13 changes: 9 additions & 4 deletions libs/cluster/Server/ClusterProvider.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using System;
Expand Down Expand Up @@ -239,7 +239,9 @@ public MetricsItem[] GetReplicationInfo()
{
var (address, port) = config.GetLocalNodePrimaryAddress();
var primaryLinkStatus = clusterManager.GetPrimaryLinkStatus(config);
var replicationOffsetLag = storeWrapper.appendOnlyFile.Log.TailAddress.AggregateDiff(replicationManager.ReplicationOffset);
var replicationOffsetAccLag = storeWrapper.appendOnlyFile.Log.TailAddress.AggregateDiff(replicationManager.ReplicationOffset);
var replicationOffsetVectorLag = storeWrapper.appendOnlyFile.Log.TailAddress.Diff(replicationManager.ReplicationOffset);
var rcm = storeWrapper.appendOnlyFile.readConsistencyManager;
replicationInfo.Add(new("master_host", address));
replicationInfo.Add(new("master_port", port.ToString()));
replicationInfo.Add(primaryLinkStatus[0]);
Expand All @@ -250,8 +252,11 @@ public MetricsItem[] GetReplicationInfo()
replicationInfo.Add(new("slave_read_only", "1"));
replicationInfo.Add(new("replica_announced", "1"));
replicationInfo.Add(new("master_sync_last_io_seconds_ago", replicationManager.LastPrimarySyncSeconds.ToString()));
replicationInfo.Add(new("replication_offset_lag", replicationOffsetLag.ToString()));
replicationInfo.Add(new("replication_offset_max_lag", storeWrapper.runtimeConfig.GetInt(ServerConfigType.REPLICATION_OFFSET_MAX_LAG).ToString()));
replicationInfo.Add(new("replication_offset_vector_lag", replicationOffsetVectorLag.ToString()));
replicationInfo.Add(new("replication_offset_acc_lag", replicationOffsetAccLag.ToString()));
replicationInfo.Add(new("aof_replay_max_lag_bytes", storeWrapper.serverOptions.AofReplayMaxLagBytes.ToString()));
replicationInfo.Add(new("physical_sublog_max_sequence_vector", rcm == null ? "-1" : rcm.GetPhysicalSublogMaxSequenceVector()));
replicationInfo.Add(new("physical_sublog_max_drift_sequence_vector", rcm == null ? "-1" : rcm.GetPhysicalSublogMaxDriftSequenceVector()));
}
else
{
Expand Down
5 changes: 0 additions & 5 deletions libs/cluster/Server/Failover/ReplicaFailoverSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ private async Task<bool> TakeOverAsPrimaryAsync()
if (clusterProvider.serverOptions.AofPhysicalSublogCount > 1)
{
clusterProvider.storeWrapper.appendOnlyFile.ResetSequenceNumberGenerator();
await clusterProvider.storeWrapper.TaskManager.CancelAsync(TaskType.AdvanceTimeReplicaTask).ConfigureAwait(false);
}

// Initialize checkpoint history
Expand All @@ -161,10 +160,6 @@ private async Task<bool> TakeOverAsPrimaryAsync()

_ = clusterProvider.BumpAndWaitForEpochTransitionAsync().ConfigureAwait(false);

// Stop advance time task when reconfiguring node to be replica
if (clusterProvider.storeWrapper.serverOptions.AofPhysicalSublogCount > 1)
await clusterProvider.storeWrapper.TaskManager.CancelAsync(TaskType.AdvanceTimeReplicaTask).ConfigureAwait(false);

// Resume all background maintenance that were possibly shutdown when this node became a replica
clusterProvider.storeWrapper.StartPrimaryTasks();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,28 @@ public AofAddress PreviousAddress
}

/// <summary>
/// Return previous address for a specific sublog without copying the full AofAddress struct
/// Return previous address for a specific sublog without copying the full AofAddress struct.
/// This is the address actually shipped (Consume'd) to the replica, used to bound AOF
/// truncation conservatively.
/// </summary>
/// <param name="physicalSublogIdx">Index of the physical sublog.</param>
/// <returns>The previous address of the specified sublog's sync task.</returns>
public long GetPreviousAddress(int physicalSublogIdx) => aofSyncTasks[physicalSublogIdx].PreviousAddress;

/// <summary>
/// Return start address for a specific sublog without copying the full AofAddress struct
/// Return the shipped watermark address for a specific sublog (the monotonic high-water this
/// replica's sync task has shipped/scanned past), used to drive the backpressure gate.
/// </summary>
/// <param name="physicalSublogIdx">Index of the physical sublog.</param>
/// <returns>The start address of the specified sublog's sync task.</returns>
public long GetStartAddress(int physicalSublogIdx) => aofSyncTasks[physicalSublogIdx].StartAddress;
/// <returns>The shipped watermark address of the specified sublog's sync task.</returns>
public long GetShippedWatermarkAddress(int physicalSublogIdx) => aofSyncTasks[physicalSublogIdx].ShippedWatermarkAddress;

/// <summary>
/// Replica endpoint
/// Return start address for a specific sublog without copying the full AofAddress struct
/// </summary>
readonly IPEndPoint endPoint;
/// <param name="physicalSublogIdx">Index of the physical sublog.</param>
/// <returns>The start address of the specified sublog's sync task.</returns>
public long GetStartAddress(int physicalSublogIdx) => aofSyncTasks[physicalSublogIdx].StartAddress;

public AofSyncDriver(
ClusterProvider clusterProvider,
Expand All @@ -100,13 +105,12 @@ public AofSyncDriver(
this.aofSyncDriverStore = aofSyncDriverStore;
this.localNodeId = localNodeId;
this.remoteNodeId = remoteNodeId;
this.endPoint = endPoint;
cts = new();
this.logger = logger;

aofSyncTasks = new AofSyncTask[clusterProvider.serverOptions.AofPhysicalSublogCount];
for (var physicalSublogIdx = 0; physicalSublogIdx < aofSyncTasks.Length; physicalSublogIdx++)
aofSyncTasks[physicalSublogIdx] = new AofSyncTask(clusterProvider, physicalSublogIdx, endPoint, startAddress[physicalSublogIdx], localNodeId, remoteNodeId, cts, logger);
aofSyncTasks[physicalSublogIdx] = new AofSyncTask(clusterProvider, aofSyncDriverStore, physicalSublogIdx, endPoint, startAddress[physicalSublogIdx], localNodeId, remoteNodeId, cts, logger);
}

/// <summary>
Expand Down Expand Up @@ -152,10 +156,9 @@ public async Task RunAsync()
}
else
{
var tasks = new Task[aofSyncTasks.Length + 1];
tasks[0] = AdvancePhysicalSublogTimeAsync();
var tasks = new Task[aofSyncTasks.Length];
for (var i = 0; i < aofSyncTasks.Length; i++)
tasks[i + 1] = aofSyncTasks[i].RunAofSyncTaskAsync(this);
tasks[i] = aofSyncTasks[i].RunAofSyncTaskAsync(this);

_ = await Task.WhenAny(tasks).ConfigureAwait(false);
}
Expand All @@ -174,60 +177,6 @@ public async Task RunAsync()
}
}

/// <summary>
/// Advance physical sublog time background task.
/// </summary>
/// <returns></returns>
/// <exception cref="GarnetException"></exception>
/// <seealso cref="T:Garnet.cluster.ReplicationManager.AdvanceTime"/>
async Task AdvancePhysicalSublogTimeAsync()
{
var enteredMonitor = false;
var client = new GarnetClientSession(
endPoint,
clusterProvider.replicationManager.GetAofSyncNetworkBufferSettings,
clusterProvider.replicationManager.GetNetworkPool,
tlsOptions: this.clusterProvider.serverOptions.TlsOptions?.TlsClientOptions,
authUsername: this.clusterProvider.ClusterUsername,
authPassword: this.clusterProvider.ClusterPassword,
logger: logger);

try
{
enteredMonitor = activeWorkerMonitor.TryEnter();
if (!enteredMonitor)
throw new GarnetException($"Failed to acquire read lock at {nameof(AdvancePhysicalSublogTimeAsync)}");

// Connect to replica
await client.ConnectAsync((int)clusterProvider.serverOptions.ReplicaSyncTimeout.TotalMilliseconds, cts.Token).ConfigureAwait(false);

var appendOnlyFile = clusterProvider.storeWrapper.appendOnlyFile;
var previousTailAddress = AofAddress.Create(appendOnlyFile.Log.Size, 0);

while (!cts.IsCancellationRequested)
{
await Task.Delay(clusterProvider.storeWrapper.runtimeConfig.GetInt(ServerConfigType.AOF_TAIL_WITNESS_FREQ), cts.Token).ConfigureAwait(false);
var currentTailAddress = appendOnlyFile.Log.TailAddress;
var newWrites = previousTailAddress.AnyLesser(currentTailAddress);

if (newWrites)
{
var sequenceNumber = appendOnlyFile.GetLargerThanMaximumSequenceNumber();
_ = await client.ExecuteClusterAdvanceTime(sequenceNumber, currentTailAddress.Span).
WaitAsync(clusterProvider.serverOptions.ReplicaSyncTimeout, cts.Token).
ConfigureAwait(false);
previousTailAddress.MonotonicUpdate(ref currentTailAddress);
}
}
}
finally
{
if (enteredMonitor)
_ = activeWorkerMonitor.Exit();
client?.Dispose();
}
}

#region DisklesSyncInterface
public async Task ConnectClientsAsync()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,80 @@ public List<RoleInfo> GetReplicaInfo(in AofAddress PrimaryReplicationOffset)
return replicaInfo;
}

/// <summary>
/// Publish one physical sublog's minimum shipped address across all attached replicas into
/// the AOF backpressure gate (long.MaxValue when none is attached, which releases it). A
/// sync task calls this for its own sublog as it ships, so only the watermark of the sublog
/// that made progress is recomputed and written. Appenders self-check their own tail against
/// the watermark, so this is a freshness hint, never a correctness action: a stale value
/// makes appenders stall sooner, not later. Must not be called while holding the store lock.
/// </summary>
/// <param name="physicalSublogIdx"></param>
internal void PublishShippedAddress(int physicalSublogIdx)
{
var appendOnlyFile = clusterProvider.storeWrapper.appendOnlyFile;
var backpressure = appendOnlyFile?.backpressure;
if (backpressure == null)
return;

var minShipped = long.MaxValue;
_lock.ReadLock();
try
{
if (!_disposed && numDrivers > 0)
{
for (var i = 0; i < numDrivers; i++)
minShipped = Math.Min(minShipped, syncDrivers[i].GetShippedWatermarkAddress(physicalSublogIdx));
}
}
finally
{
_lock.ReadUnlock();
}

backpressure.PublishShippedAddress(physicalSublogIdx, minShipped);
}

/// <summary>
/// Publish every physical sublog's minimum shipped address across all attached replicas into
/// the AOF backpressure gate (long.MaxValue per sublog when none is attached, or during
/// dispose, which releases the gate). The store calls this whenever the driver set changes,
/// since attaching or detaching a replica can change every sublog's minimum at once. Must
/// not be called while holding the store lock.
/// </summary>
internal void PublishShippedAddresses()
{
var appendOnlyFile = clusterProvider.storeWrapper.appendOnlyFile;
var backpressure = appendOnlyFile?.backpressure;
if (backpressure == null)
return;

var sublogCount = clusterProvider.serverOptions.AofPhysicalSublogCount;
_lock.ReadLock();
try
{
if (_disposed || numDrivers == 0)
{
for (var physicalSublogIdx = 0; physicalSublogIdx < sublogCount; physicalSublogIdx++)
backpressure.PublishShippedAddress(physicalSublogIdx, long.MaxValue);
}
else
{
for (var physicalSublogIdx = 0; physicalSublogIdx < sublogCount; physicalSublogIdx++)
{
var minShipped = long.MaxValue;
for (var i = 0; i < numDrivers; i++)
minShipped = Math.Min(minShipped, syncDrivers[i].GetShippedWatermarkAddress(physicalSublogIdx));
backpressure.PublishShippedAddress(physicalSublogIdx, minShipped);
}
}
}
finally
{
_lock.ReadUnlock();
}
}

/// <summary>
/// Dispose the AofSyncDriverStore
/// </summary>
Expand Down Expand Up @@ -234,6 +308,10 @@ public void Dispose()
}
numDrivers = 0;
Array.Clear(syncDrivers);

// With no drivers attached, PublishShippedAddresses writes a max watermark per sublog,
// making every appender's computed lag non-positive so none stalls on the gate.
PublishShippedAddresses();
}

/// <summary>
Expand Down Expand Up @@ -333,6 +411,9 @@ public bool TryAddReplicationDriver(string remoteNodeId, ref AofAddress startAdd
}
}

if (success)
PublishShippedAddresses();

return success;
}

Expand Down Expand Up @@ -442,6 +523,9 @@ public bool TryAddReplicationDrivers(ReplicaSyncSession[] replicaSyncSessions, r
}
}

if (success)
PublishShippedAddresses();

return true;
}

Expand Down Expand Up @@ -491,6 +575,10 @@ public bool TryRemove(AofSyncDriver aofSyncDriver)
{
_lock.WriteUnlock();
}

if (success)
PublishShippedAddresses();

return success;
}

Expand Down Expand Up @@ -562,6 +650,10 @@ public void Reset()
{
_lock.WriteUnlock();
}

// No drivers remain, so PublishShippedAddresses writes a max watermark per sublog and
// no appender stays stalled on the gate.
PublishShippedAddresses();
}

[Conditional("DEBUG")]
Expand Down
Loading
Loading