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
27 changes: 27 additions & 0 deletions src/core/Akka.Remote.Tests/AckedDeliverySpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,33 @@ public void SendBuffer_must_keep_NACKed_messages_in_buffer_if_selective_nacks_ar
Assert.True(b7.Nacked.Count == 0);
}

[Fact]
public void SendBuffer_must_ignore_stale_ack_from_previous_association()
{
// Reproduces GitHub #6414: a fresh send buffer (MaxSeq = -1) receives a stale ACK
// from a previous association. This must be a no-op, not a throw.
var b0 = new AckedSendBuffer<Sequenced>(10);

// ACK[0] from stale receiver state - MaxSeq is -1, CumulativeAck is 0
var result = b0.Acknowledge(new Ack(new SeqNo(0)));
Assert.Same(b0, result); // should return the same instance (no-op)
}

[Fact]
public void SendBuffer_must_ignore_stale_ack_when_buffer_has_messages_with_lower_seqnos()
{
// A buffer with some messages receives a stale ACK referencing a higher sequence
// number than anything buffered - should be a no-op.
var b0 = new AckedSendBuffer<Sequenced>(10);
var msg0 = Msg(0);
var msg1 = Msg(1);
var b1 = b0.Buffer(msg0).Buffer(msg1); // MaxSeq = 1

// Stale ACK referencing SeqNo(5) which we never sent
var result = b1.Acknowledge(new Ack(new SeqNo(5)));
Assert.Same(b1, result); // should return the same instance (no-op)
}

[Fact]
public void SendBuffer_must_throw_exception_if_nonbuffered_sequence_number_is_NACKed()
{
Expand Down
6 changes: 5 additions & 1 deletion src/core/Akka.Remote/AckedDelivery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -431,9 +431,13 @@ public AckedSendBuffer(int capacity, SeqNo maxSeq, IImmutableList<T> nacked, IIm
/// <returns>An updated buffer containing the remaining unacknowledged messages</returns>
public AckedSendBuffer<T> Acknowledge(Ack ack)
{
// If the ACK references a sequence number higher than anything we've sent,
// it's a stale ACK from a previous association. The buffer is empty or has
// only messages with lower sequence numbers — there's nothing to acknowledge.
// Throwing here would cause an irrecoverable quarantine (see GitHub #6414).
if (ack.CumulativeAck > MaxSeq)
{
throw new ArgumentException(nameof(ack), $"Highest SEQ so far was {MaxSeq} but cumulative ACK is {ack.CumulativeAck}");
return this;
}

var newNacked = ack.Nacks.Count == 0
Expand Down
6 changes: 6 additions & 0 deletions src/core/Akka.Remote/Endpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,12 @@ private void Receiving()
try
{
_resendBuffer = _resendBuffer.Acknowledge(ack);
if (ack.CumulativeAck > _resendBuffer.MaxSeq)
{
_log.Warning(
"Ignoring stale ACK [{0}] for send buffer [{1}] - likely from a previous association to [{2}]",
ack, _resendBuffer, _remoteAddress);
}
}
catch (Exception ex)
{
Expand Down
Loading