blankensteiner commented on a change in pull request #67: URL: https://github.com/apache/pulsar-dotpulsar/pull/67#discussion_r553295151
########## File path: src/DotPulsar/Internal/InactiveUnackedMessageTracker.cs ########## @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace DotPulsar.Internal +{ + using Abstractions; + using DotPulsar.Abstractions; + using System.Threading; + using System.Threading.Tasks; + + public class InactiveUnackedMessageTracker : IUnackedMessageTracker Review comment: Let's make it sealed. ########## File path: tests/DotPulsar.Tests/Internal/MessageAcksTrackerTests.cs ########## @@ -0,0 +1,158 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace DotPulsar.Tests.Internal +{ + using AutoFixture; + using AutoFixture.AutoNSubstitute; + using DotPulsar.Abstractions; + using DotPulsar.Internal; + using NSubstitute; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Threading; + using System.Threading.Tasks; + using Xunit; + + public class UnackedMessageTrackerTests + { + + [Fact] + public async void Start_GivenAMessageIdIsNotAcked_ShouldRedeliver() + { + //Arrange + var fixture = new Fixture(); + fixture.Customize(new AutoNSubstituteCustomization()); + var consumer = Substitute.For<IConsumer>(); + var messageId = MessageId.Latest; + var cts = new CancellationTokenSource(); + Review comment: Let's remove this empty line ########## File path: tests/DotPulsar.Tests/Internal/MessageAcksTrackerTests.cs ########## @@ -0,0 +1,158 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace DotPulsar.Tests.Internal +{ + using AutoFixture; + using AutoFixture.AutoNSubstitute; + using DotPulsar.Abstractions; + using DotPulsar.Internal; + using NSubstitute; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Threading; + using System.Threading.Tasks; + using Xunit; + + public class UnackedMessageTrackerTests + { + + [Fact] + public async void Start_GivenAMessageIdIsNotAcked_ShouldRedeliver() + { + //Arrange + var fixture = new Fixture(); + fixture.Customize(new AutoNSubstituteCustomization()); + var consumer = Substitute.For<IConsumer>(); + var messageId = MessageId.Latest; + var cts = new CancellationTokenSource(); + + + var tracker = new UnackedMessageTracker( + TimeSpan.FromMilliseconds(10), + TimeSpan.FromMilliseconds(1)); + + //Act + tracker.Add(messageId); + cts.CancelAfter(20); + try { await tracker.Start(consumer, cts.Token); } + catch (TaskCanceledException) { } + + //Assert + await consumer + .Received(1) + .RedeliverUnacknowledgedMessages( + Arg.Is(EquivalentTo(new List<MessageId>() { messageId })), + Arg.Any<CancellationToken>()); + } + + [Fact] + public async void Start_GivenAMessageIdIsAckedWithinTimeout_ShouldNotRedeliver() + { + //Arrange + var fixture = new Fixture(); + fixture.Customize(new AutoNSubstituteCustomization()); + var consumer = Substitute.For<IConsumer>(); + var messageId = MessageId.Latest; + var cts = new CancellationTokenSource(); + + + var tracker = new UnackedMessageTracker( + TimeSpan.FromMilliseconds(10), + TimeSpan.FromMilliseconds(1)); + + //Act + tracker.Add(messageId); + cts.CancelAfter(20); + var _ = Task.Delay(5).ContinueWith(_ => tracker.Ack(messageId)); + try { await tracker.Start(consumer, cts.Token); } + catch (TaskCanceledException) { } + + //Assert + await consumer + .DidNotReceive() + .RedeliverUnacknowledgedMessages( + Arg.Any<IEnumerable<MessageId>>(), + Arg.Any<CancellationToken>()); + } + + [Fact] + public async void Start_GivenAMessageIdIsNotAckedWithinTimeout_ShouldRedeliver() + { + //Arrange + var fixture = new Fixture(); + fixture.Customize(new AutoNSubstituteCustomization()); + var consumer = Substitute.For<IConsumer>(); + var messageId = MessageId.Latest; + var cts = new CancellationTokenSource(); + Review comment: Let's remove this empty line ########## File path: src/DotPulsar/Internal/UnackedMessageTracker.cs ########## @@ -0,0 +1,115 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace DotPulsar.Internal +{ + using Abstractions; + using DotPulsar.Abstractions; + using System; + using System.Collections.Concurrent; + using System.Diagnostics; + using System.Linq; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + public readonly struct AwaitingAck + { + public MessageId MessageId { get; } + public long Timestamp { get; } + + public AwaitingAck(MessageId messageId) + { + MessageId = messageId; + Timestamp = Stopwatch.GetTimestamp(); + } + + public TimeSpan Elapsed => TimeSpan.FromTicks( + (long) ((Stopwatch.GetTimestamp() - Timestamp) / (double) Stopwatch.Frequency * TimeSpan.TicksPerSecond)); + } + + public sealed class UnackedMessageTracker : IUnackedMessageTracker Review comment: For the class and interface names, let's just the full wording: [I]UnacknowledgedMessageTracner. ########## File path: tests/DotPulsar.Tests/Internal/MessageAcksTrackerTests.cs ########## @@ -0,0 +1,158 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace DotPulsar.Tests.Internal +{ + using AutoFixture; + using AutoFixture.AutoNSubstitute; + using DotPulsar.Abstractions; + using DotPulsar.Internal; + using NSubstitute; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Threading; + using System.Threading.Tasks; + using Xunit; + + public class UnackedMessageTrackerTests + { + Review comment: Let's remove this empty line ########## File path: tests/DotPulsar.Tests/Internal/MessageAcksTrackerTests.cs ########## @@ -0,0 +1,158 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace DotPulsar.Tests.Internal +{ + using AutoFixture; + using AutoFixture.AutoNSubstitute; + using DotPulsar.Abstractions; + using DotPulsar.Internal; + using NSubstitute; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Threading; + using System.Threading.Tasks; + using Xunit; + + public class UnackedMessageTrackerTests + { + + [Fact] + public async void Start_GivenAMessageIdIsNotAcked_ShouldRedeliver() + { + //Arrange + var fixture = new Fixture(); + fixture.Customize(new AutoNSubstituteCustomization()); + var consumer = Substitute.For<IConsumer>(); + var messageId = MessageId.Latest; + var cts = new CancellationTokenSource(); + + + var tracker = new UnackedMessageTracker( + TimeSpan.FromMilliseconds(10), + TimeSpan.FromMilliseconds(1)); + + //Act + tracker.Add(messageId); + cts.CancelAfter(20); + try { await tracker.Start(consumer, cts.Token); } + catch (TaskCanceledException) { } + + //Assert + await consumer + .Received(1) + .RedeliverUnacknowledgedMessages( + Arg.Is(EquivalentTo(new List<MessageId>() { messageId })), + Arg.Any<CancellationToken>()); + } + + [Fact] + public async void Start_GivenAMessageIdIsAckedWithinTimeout_ShouldNotRedeliver() + { + //Arrange + var fixture = new Fixture(); + fixture.Customize(new AutoNSubstituteCustomization()); + var consumer = Substitute.For<IConsumer>(); + var messageId = MessageId.Latest; + var cts = new CancellationTokenSource(); + Review comment: Let's remove this empty line ########## File path: tests/DotPulsar.Tests/Internal/MessageAcksTrackerTests.cs ########## @@ -0,0 +1,158 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace DotPulsar.Tests.Internal +{ + using AutoFixture; + using AutoFixture.AutoNSubstitute; + using DotPulsar.Abstractions; + using DotPulsar.Internal; + using NSubstitute; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Threading; + using System.Threading.Tasks; + using Xunit; + + public class UnackedMessageTrackerTests + { + + [Fact] + public async void Start_GivenAMessageIdIsNotAcked_ShouldRedeliver() + { + //Arrange + var fixture = new Fixture(); + fixture.Customize(new AutoNSubstituteCustomization()); + var consumer = Substitute.For<IConsumer>(); + var messageId = MessageId.Latest; + var cts = new CancellationTokenSource(); + + + var tracker = new UnackedMessageTracker( + TimeSpan.FromMilliseconds(10), + TimeSpan.FromMilliseconds(1)); + + //Act + tracker.Add(messageId); + cts.CancelAfter(20); + try { await tracker.Start(consumer, cts.Token); } + catch (TaskCanceledException) { } + + //Assert + await consumer + .Received(1) + .RedeliverUnacknowledgedMessages( + Arg.Is(EquivalentTo(new List<MessageId>() { messageId })), + Arg.Any<CancellationToken>()); + } + + [Fact] + public async void Start_GivenAMessageIdIsAckedWithinTimeout_ShouldNotRedeliver() + { + //Arrange + var fixture = new Fixture(); + fixture.Customize(new AutoNSubstituteCustomization()); + var consumer = Substitute.For<IConsumer>(); + var messageId = MessageId.Latest; + var cts = new CancellationTokenSource(); + + + var tracker = new UnackedMessageTracker( + TimeSpan.FromMilliseconds(10), + TimeSpan.FromMilliseconds(1)); + + //Act + tracker.Add(messageId); + cts.CancelAfter(20); + var _ = Task.Delay(5).ContinueWith(_ => tracker.Ack(messageId)); + try { await tracker.Start(consumer, cts.Token); } + catch (TaskCanceledException) { } + + //Assert + await consumer + .DidNotReceive() + .RedeliverUnacknowledgedMessages( + Arg.Any<IEnumerable<MessageId>>(), + Arg.Any<CancellationToken>()); + } + + [Fact] + public async void Start_GivenAMessageIdIsNotAckedWithinTimeout_ShouldRedeliver() + { + //Arrange + var fixture = new Fixture(); + fixture.Customize(new AutoNSubstituteCustomization()); + var consumer = Substitute.For<IConsumer>(); + var messageId = MessageId.Latest; + var cts = new CancellationTokenSource(); + + + var tracker = new UnackedMessageTracker( + TimeSpan.FromMilliseconds(10), + TimeSpan.FromMilliseconds(1)); + + //Act + tracker.Add(messageId); + cts.CancelAfter(20); + + var _ = Task.Delay(15).ContinueWith(_ => tracker.Ack(messageId)); + try { await tracker.Start(consumer, cts.Token); } + catch (TaskCanceledException) { } + + //Assert + await consumer + .Received(1) + .RedeliverUnacknowledgedMessages( + Arg.Any<IEnumerable<MessageId>>(), + Arg.Any<CancellationToken>()); + } + + [Fact] + public async void Start_GivenAMessageIdIsNotAckedWithinTimeout_ShouldRedeliverOnlyOnce() + { + //Arrange + var fixture = new Fixture(); + fixture.Customize(new AutoNSubstituteCustomization()); + var consumer = Substitute.For<IConsumer>(); + var messageId = MessageId.Latest; + var cts = new CancellationTokenSource(); + Review comment: Let's remove this empty line ########## File path: src/DotPulsar/Internal/MessageQueue.cs ########## @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace DotPulsar.Internal +{ + using Abstractions; + using System; + using System.Threading; + using System.Threading.Tasks; + + public sealed class MessageQueue : IMessageQueue, IDequeue<MessagePackage>, IDisposable Review comment: Is "IMessageQueue" enough here since it inherits the other two interfaces? ########## File path: src/DotPulsar/Internal/Abstractions/IUnackedMessageTracker.cs ########## @@ -0,0 +1,30 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace DotPulsar.Internal.Abstractions +{ + using DotPulsar.Abstractions; + using System.Threading.Tasks; + using System.Threading; + using System; + + public interface IUnackedMessageTracker : IDisposable + { + void Add(MessageId messageId); + + void Ack(MessageId messageId); Review comment: Let's use "Acknowledge" instead of the shorter "Ack", to be consistent with the public interfaces. ########## File path: src/DotPulsar/Internal/UnackedMessageTracker.cs ########## @@ -0,0 +1,115 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace DotPulsar.Internal +{ + using Abstractions; + using DotPulsar.Abstractions; + using System; + using System.Collections.Concurrent; + using System.Diagnostics; + using System.Linq; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + public readonly struct AwaitingAck + { + public MessageId MessageId { get; } + public long Timestamp { get; } + + public AwaitingAck(MessageId messageId) + { + MessageId = messageId; + Timestamp = Stopwatch.GetTimestamp(); + } + + public TimeSpan Elapsed => TimeSpan.FromTicks( + (long) ((Stopwatch.GetTimestamp() - Timestamp) / (double) Stopwatch.Frequency * TimeSpan.TicksPerSecond)); + } + + public sealed class UnackedMessageTracker : IUnackedMessageTracker + { + private readonly TimeSpan _ackTimeout; + private readonly TimeSpan _pollingTimeout; + private readonly ConcurrentQueue<AwaitingAck> _awaitingAcks; + private readonly List<MessageId> _acked; + private readonly CancellationTokenSource _cancellationTokenSource; + + public UnackedMessageTracker(TimeSpan ackTimeout, TimeSpan pollingTimeout) + { + _ackTimeout = ackTimeout; + _pollingTimeout = pollingTimeout; + _awaitingAcks = new ConcurrentQueue<AwaitingAck>(); + _acked = new List<MessageId>(); + _cancellationTokenSource = new CancellationTokenSource(); + } + + public void Add(MessageId messageId) + { + _awaitingAcks.Enqueue(new AwaitingAck(messageId)); + } + + public void Ack(MessageId messageId) + { + // We only need to store the highest cumulative ack we see (if there is one) + // and the MessageIds not included by that cumulative ack. + _acked.Add(messageId); + } + + public Task Start(IConsumer consumer, CancellationToken cancellationToken = default) + { + CancellationToken token = + CancellationTokenSource.CreateLinkedTokenSource( + _cancellationTokenSource.Token, cancellationToken).Token; + + return Task.Run(async () => + { + while (!token.IsCancellationRequested) + { + var messages = CheckUnackedMessages(); + + if (messages.Count() > 0) + await consumer.RedeliverUnacknowledgedMessages(messages, token); + + await Task.Delay(_pollingTimeout, token); + } + }, token); + } + + private IEnumerable<MessageId> CheckUnackedMessages() + { + var result = new List<MessageId>(); + + while (_awaitingAcks.TryPeek(out AwaitingAck awaiting) + && awaiting.Elapsed > _ackTimeout) + { + if (_awaitingAcks.TryDequeue(out awaiting)) + { + if (!_acked.Contains(awaiting.MessageId)) + result.Add(awaiting.MessageId); + else + _acked.Remove(awaiting.MessageId); + } + } + + return result; + } + + public void Dispose() + { + this._cancellationTokenSource.Cancel(); Review comment: No need to use "this." ########## File path: src/DotPulsar/Internal/ConsumerChannel.cs ########## @@ -108,6 +108,9 @@ public async Task Send(CommandAck command, CancellationToken cancellationToken) } command.ConsumerId = _id; + + _queue.Acknowledge(new MessageId(messageId)); Review comment: Ah yes, we might have to implement IEquatable and IComparable on MessageIdData also then. When receiving an Ack, you also have to know if it is 'Individual' or 'Cumulative'. ---------------------------------------------------------------- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. For queries about this service, please contact Infrastructure at: us...@infra.apache.org