Skip to content
Open
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
53 changes: 0 additions & 53 deletions foreign/csharp/Iggy_SDK/ConnectionStream/TcpConnectionStream.cs

This file was deleted.

46 changes: 46 additions & 0 deletions foreign/csharp/Iggy_SDK/Consumers/IggyConsumer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using Apache.Iggy.IggyClient;
using Apache.Iggy.Kinds;
using Apache.Iggy.Utils;
using Apache.Iggy.Vsr;
using Microsoft.Extensions.Logging;

namespace Apache.Iggy.Consumers;
Expand All @@ -46,6 +47,13 @@ public partial class IggyConsumer : IAsyncDisposable
private int _disposeState;
private volatile bool _isInitialized;
private volatile bool _joinedConsumerGroup;

/// <summary>
/// Consensus session epoch the group membership was established under. A later epoch means the server
/// session that held the membership is gone and the group must be rejoined.
/// </summary>
private ulong _joinedSessionEpoch;

private long _lastPolledAtMs;

/// <summary>Whether this consumer has been initialized via <see cref="InitAsync" />.</summary>
Expand Down Expand Up @@ -263,8 +271,13 @@ private async Task InitializeConsumerGroupAsync(CancellationToken ct = default)
return;
}

// Captured before the join: a session reset racing the join lands the membership on a later epoch, and
// the mismatch triggers one redundant (idempotent) rejoin instead of a missed one.
var sessionEpoch = (_client as ISessionEpochProvider)?.SessionEpoch ?? 0;

if (_config.Consumer.Type == ConsumerType.Consumer)
{
_joinedSessionEpoch = sessionEpoch;
_joinedConsumerGroup = true;
return;
}
Expand Down Expand Up @@ -306,6 +319,7 @@ private async Task InitializeConsumerGroupAsync(CancellationToken ct = default)
await _client.JoinConsumerGroupAsync(_config.StreamId, _config.TopicId,
Identifier.String(_consumerGroupName), ct);

_joinedSessionEpoch = sessionEpoch;
_joinedConsumerGroup = true;
LogConsumerGroupJoined(_consumerGroupName);
}
Expand Down Expand Up @@ -505,6 +519,38 @@ private async Task OnClientConnectionStateChangedAsync(ConnectionStateChangedEve
await _connectionStateSemaphore.WaitAsync();
try
{
if (_client is ISessionEpochProvider epochProvider)
{
// A plain consumer holds no membership, so its flag must never be cleared: nothing would ever
// set it again and every later poll would be skipped.
if (_config.Consumer.Type == ConsumerType.Consumer)
{
return;
}

if (e.CurrentState != ConnectionState.Authenticated)
{
// Polling under a dropped session would be refused by the server for as long as the
// reconnect takes to re-authenticate, so the membership is surrendered up front.
if (e.CurrentState == ConnectionState.Disconnected)
{
_joinedConsumerGroup = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this clear is still edge-based inside the branch that exists to stop depending on edges. state events can be published concurrently (SetConnectionStateAsync is a plain read-modify-write with the publish after the write), so a late Disconnected can land after the Authenticated arm already rejoined and stamped the new epoch - the flag stays false and polling silently stops until the next Authenticated event, which on a healthy connection never comes. gating the poll on _joinedSessionEpoch == provider.SessionEpoch for group consumers removes the dependence on event arrival; plain consumers must keep the early return above (they never re-stamp), and the epoch read then happens outside _connectionStateSemaphore, so access it with Interlocked.Read/Exchange(ref ulong).

}

return;
}

if (_joinedConsumerGroup && epochProvider.SessionEpoch == _joinedSessionEpoch)
{
return;
}

_joinedConsumerGroup = false;
await RejoinConsumerGroupOnReconnectionAsync();

return;
}

if (e.CurrentState == ConnectionState.Disconnected)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this fallback is unreachable in-tree: HttpMessageStream.SubscribeConnectionEvents has an empty body and never publishes, and an out-of-tree IIggyClient cannot implement the internal ISessionEpochProvider to take the branch above. the ISessionEpochProvider doc also says HTTP consumers fall back to connection-state edges, which is not true - they get no events at all. either make the interface public, or delete this and document group rejoin as built-in-TCP-only; if it stays, it needs the same plain-consumer carve-out as the epoch arm.

{
_joinedConsumerGroup = false;
Expand Down
Loading
Loading