Skip to content

Commit e482da2

Browse files
[FIXED] (2.11) Propose message delete for clustered interest stream (#6140)
For an Interest or WorkQueue stream messages will be removed once all consumers that need to receive a message have acked it. For a clustered stream each consumer would ack and remove a message by themselves. This can be problematic since that introduces different ordering between servers. For example when using DiscardOld with MaxMsgs, which could result in stream desync. Proposing the message removal ensures ordering between servers. Signed-off-by: Maurice van Veen <[email protected]> --------- Signed-off-by: Maurice van Veen <[email protected]> Signed-off-by: Neil Twigg <[email protected]> Co-authored-by: Neil Twigg <[email protected]>
1 parent 45ee8c4 commit e482da2

File tree

4 files changed

+142
-17
lines changed

4 files changed

+142
-17
lines changed

server/consumer.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5544,6 +5544,7 @@ func (o *consumer) isMonitorRunning() bool {
55445544

55455545
// If we detect that our ackfloor is higher than the stream's last sequence, return this error.
55465546
var errAckFloorHigherThanLastSeq = errors.New("consumer ack floor is higher than streams last sequence")
5547+
var errAckFloorInvalid = errors.New("consumer ack floor is invalid")
55475548

55485549
// If we are a consumer of an interest or workqueue policy stream, process that state and make sure consistent.
55495550
func (o *consumer) checkStateForInterestStream(ss *StreamState) error {
@@ -5573,7 +5574,7 @@ func (o *consumer) checkStateForInterestStream(ss *StreamState) error {
55735574
asflr := state.AckFloor.Stream
55745575
// Protect ourselves against rolling backwards.
55755576
if asflr&(1<<63) != 0 {
5576-
return nil
5577+
return errAckFloorInvalid
55775578
}
55785579

55795580
// Check if the underlying stream's last sequence is less than our floor.
@@ -5592,6 +5593,7 @@ func (o *consumer) checkStateForInterestStream(ss *StreamState) error {
55925593
fseq = chkfloor
55935594
}
55945595

5596+
var retryAsflr uint64
55955597
for seq = fseq; asflr > 0 && seq <= asflr; seq++ {
55965598
if filters != nil {
55975599
_, nseq, err = store.LoadNextMsgMulti(filters, seq, &smv)
@@ -5604,15 +5606,24 @@ func (o *consumer) checkStateForInterestStream(ss *StreamState) error {
56045606
}
56055607
// Only ack though if no error and seq <= ack floor.
56065608
if err == nil && seq <= asflr {
5607-
mset.ackMsg(o, seq)
5609+
didRemove := mset.ackMsg(o, seq)
5610+
// Removing the message could fail. For example if we're behind on stream applies.
5611+
// Overwrite retry floor (only the first time) to allow us to check next time if the removal was successful.
5612+
if didRemove && retryAsflr == 0 {
5613+
retryAsflr = seq
5614+
}
56085615
}
56095616
}
5617+
// If retry floor was not overwritten, set to ack floor+1, we don't need to account for any retries below it.
5618+
if retryAsflr == 0 {
5619+
retryAsflr = asflr + 1
5620+
}
56105621

56115622
o.mu.Lock()
56125623
// Update our check floor.
56135624
// Check floor must never be greater than ack floor+1, otherwise subsequent calls to this function would skip work.
5614-
if asflr+1 > o.chkflr {
5615-
o.chkflr = asflr + 1
5625+
if retryAsflr > o.chkflr {
5626+
o.chkflr = retryAsflr
56165627
}
56175628
// See if we need to process this update if our parent stream is not a limits policy stream.
56185629
state, _ = o.store.State()

server/jetstream_cluster_4_test.go

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,7 @@ func TestJetStreamClusterStreamOrphanMsgsAndReplicasDrifting(t *testing.T) {
744744
var expectedErr error
745745
var msgId string
746746
var smv StoreMsg
747-
for i, mset := range msets {
747+
for _, mset := range msets {
748748
mset.mu.RLock()
749749
sm, err := mset.store.LoadMsg(seq, &smv)
750750
mset.mu.RUnlock()
@@ -754,17 +754,14 @@ func TestJetStreamClusterStreamOrphanMsgsAndReplicasDrifting(t *testing.T) {
754754
// by all msets for that seq to prove consistency across replicas.
755755
// If one of the msets either returns no error or doesn't return
756756
// the same error, then that replica has drifted.
757-
if msgId != _EMPTY_ {
758-
t.Fatalf("Expected MsgId %q for seq %d, but got error: %v", msgId, seq, err)
759-
} else if expectedErr == nil {
757+
if expectedErr == nil {
760758
expectedErr = err
761759
} else {
762760
require_Error(t, err, expectedErr)
763761
}
764762
continue
765763
}
766-
// Only set expected msg ID if it's for the very first time.
767-
if msgId == _EMPTY_ && i == 0 {
764+
if msgId == _EMPTY_ {
768765
msgId = string(sm.hdr)
769766
} else if msgId != string(sm.hdr) {
770767
t.Fatalf("MsgIds do not match for seq %d: %q vs %q", seq, msgId, sm.hdr)
@@ -4673,3 +4670,116 @@ func TestJetStreamClusterRoutedAPIRecoverPerformance(t *testing.T) {
46734670
ljs.mu.Lock()
46744671
t.Logf("Took %s to clear %d items", time.Since(start), count)
46754672
}
4673+
4674+
func TestJetStreamClusterStreamAckMsgR1SignalsRemovedMsg(t *testing.T) {
4675+
c := createJetStreamClusterExplicit(t, "R3S", 3)
4676+
defer c.shutdown()
4677+
4678+
nc, js := jsClientConnect(t, c.randomServer())
4679+
defer nc.Close()
4680+
4681+
_, err := js.AddStream(&nats.StreamConfig{
4682+
Name: "TEST",
4683+
Subjects: []string{"foo"},
4684+
Retention: nats.WorkQueuePolicy,
4685+
Replicas: 1,
4686+
})
4687+
require_NoError(t, err)
4688+
4689+
_, err = js.AddConsumer("TEST", &nats.ConsumerConfig{
4690+
Durable: "CONSUMER",
4691+
Replicas: 1,
4692+
AckPolicy: nats.AckExplicitPolicy,
4693+
})
4694+
require_NoError(t, err)
4695+
4696+
_, err = js.Publish("foo", nil)
4697+
require_NoError(t, err)
4698+
4699+
s := c.streamLeader(globalAccountName, "TEST")
4700+
acc, err := s.lookupAccount(globalAccountName)
4701+
require_NoError(t, err)
4702+
mset, err := acc.lookupStream("TEST")
4703+
require_NoError(t, err)
4704+
o := mset.lookupConsumer("CONSUMER")
4705+
require_NotNil(t, o)
4706+
4707+
// Too high sequence, should register pre-ack and return true allowing for retries.
4708+
require_True(t, mset.ackMsg(o, 100))
4709+
4710+
var smv StoreMsg
4711+
sm, err := mset.store.LoadMsg(1, &smv)
4712+
require_NoError(t, err)
4713+
require_Equal(t, sm.subj, "foo")
4714+
4715+
// Now do a proper ack, should immediately remove the message since it's R1.
4716+
require_True(t, mset.ackMsg(o, 1))
4717+
_, err = mset.store.LoadMsg(1, &smv)
4718+
require_Error(t, err, ErrStoreMsgNotFound)
4719+
}
4720+
4721+
func TestJetStreamClusterStreamAckMsgR3SignalsRemovedMsg(t *testing.T) {
4722+
c := createJetStreamClusterExplicit(t, "R3S", 3)
4723+
defer c.shutdown()
4724+
4725+
nc, js := jsClientConnect(t, c.randomServer())
4726+
defer nc.Close()
4727+
4728+
_, err := js.AddStream(&nats.StreamConfig{
4729+
Name: "TEST",
4730+
Subjects: []string{"foo"},
4731+
Retention: nats.WorkQueuePolicy,
4732+
Replicas: 3,
4733+
})
4734+
require_NoError(t, err)
4735+
4736+
_, err = js.AddConsumer("TEST", &nats.ConsumerConfig{
4737+
Durable: "CONSUMER",
4738+
Replicas: 3,
4739+
AckPolicy: nats.AckExplicitPolicy,
4740+
})
4741+
require_NoError(t, err)
4742+
4743+
_, err = js.Publish("foo", nil)
4744+
require_NoError(t, err)
4745+
4746+
getStreamAndConsumer := func(s *Server) (*stream, *consumer, error) {
4747+
t.Helper()
4748+
acc, err := s.lookupAccount(globalAccountName)
4749+
if err != nil {
4750+
return nil, nil, err
4751+
}
4752+
mset, err := acc.lookupStream("TEST")
4753+
if err != nil {
4754+
return nil, nil, err
4755+
}
4756+
o := mset.lookupConsumer("CONSUMER")
4757+
if err != nil {
4758+
return nil, nil, err
4759+
}
4760+
return mset, o, nil
4761+
}
4762+
4763+
sl := c.consumerLeader(globalAccountName, "TEST", "CONSUMER")
4764+
sf := c.randomNonConsumerLeader(globalAccountName, "TEST", "CONSUMER")
4765+
4766+
msetL, ol, err := getStreamAndConsumer(sl)
4767+
require_NoError(t, err)
4768+
msetF, of, err := getStreamAndConsumer(sf)
4769+
require_NoError(t, err)
4770+
4771+
// Too high sequence, should register pre-ack and return true allowing for retries.
4772+
require_True(t, msetL.ackMsg(ol, 100))
4773+
require_True(t, msetF.ackMsg(of, 100))
4774+
4775+
// Let all servers ack the message.
4776+
var smv StoreMsg
4777+
for _, s := range c.servers {
4778+
mset, _, err := getStreamAndConsumer(s)
4779+
require_NoError(t, err)
4780+
require_True(t, mset.ackMsg(of, 1))
4781+
4782+
_, err = mset.store.LoadMsg(1, &smv)
4783+
require_Error(t, err, ErrStoreMsgNotFound)
4784+
}
4785+
}

server/norace_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11225,7 +11225,7 @@ func TestNoRaceJetStreamClusterCheckInterestStatePerformanceInterest(t *testing.
1122511225
}
1122611226

1122711227
require_Equal(t, checkFloor(mset.lookupConsumer("A")), 1)
11228-
require_Equal(t, checkFloor(mset.lookupConsumer("B")), 100_001)
11228+
require_Equal(t, checkFloor(mset.lookupConsumer("B")), 90_001)
1122911229
require_Equal(t, checkFloor(mset.lookupConsumer("C")), 100_001)
1123011230

1123111231
// This checks the chkflr state. For this test this should be much faster,

server/stream.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5683,16 +5683,17 @@ func (mset *stream) clearPreAck(o *consumer, seq uint64) {
56835683
}
56845684

56855685
// ackMsg is called into from a consumer when we have a WorkQueue or Interest Retention Policy.
5686-
func (mset *stream) ackMsg(o *consumer, seq uint64) {
5686+
// Returns whether the message at seq was removed as a result of the ACK.
5687+
func (mset *stream) ackMsg(o *consumer, seq uint64) bool {
56875688
if seq == 0 {
5688-
return
5689+
return false
56895690
}
56905691

56915692
// Don't make this RLock(). We need to have only 1 running at a time to gauge interest across all consumers.
56925693
mset.mu.Lock()
56935694
if mset.closed.Load() || mset.cfg.Retention == LimitsPolicy {
56945695
mset.mu.Unlock()
5695-
return
5696+
return false
56965697
}
56975698

56985699
store := mset.store
@@ -5703,7 +5704,9 @@ func (mset *stream) ackMsg(o *consumer, seq uint64) {
57035704
if seq > state.LastSeq {
57045705
mset.registerPreAck(o, seq)
57055706
mset.mu.Unlock()
5706-
return
5707+
// We have not removed the message, but should still signal so we could retry later
5708+
// since we potentially need to remove it then.
5709+
return true
57075710
}
57085711

57095712
// Always clear pre-ack if here.
@@ -5712,7 +5715,7 @@ func (mset *stream) ackMsg(o *consumer, seq uint64) {
57125715
// Make sure this sequence is not below our first sequence.
57135716
if seq < state.FirstSeq {
57145717
mset.mu.Unlock()
5715-
return
5718+
return false
57165719
}
57175720

57185721
var shouldRemove bool
@@ -5728,14 +5731,15 @@ func (mset *stream) ackMsg(o *consumer, seq uint64) {
57285731

57295732
// If nothing else to do.
57305733
if !shouldRemove {
5731-
return
5734+
return false
57325735
}
57335736

57345737
// If we are here we should attempt to remove.
57355738
if _, err := store.RemoveMsg(seq); err == ErrStoreEOF {
57365739
// This should not happen, but being pedantic.
57375740
mset.registerPreAckLock(o, seq)
57385741
}
5742+
return true
57395743
}
57405744

57415745
// Snapshot creates a snapshot for the stream and possibly consumers.

0 commit comments

Comments
 (0)