Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
acae403
Add wait-for-subscribers feature
Arkatufus May 15, 2025
e72cbd4
Merge remote-tracking branch 'upstream/dev' into Add-wait-for-subscri…
Arkatufus May 15, 2025
8f05840
Fix missing code
Arkatufus May 15, 2025
0177811
Update API approval list
Arkatufus May 15, 2025
d30b8a2
Fix HOCON errors
Arkatufus May 16, 2025
001e056
Merge branch 'dev' into Add-wait-for-subscribers-feature
Arkatufus May 16, 2025
98a6999
Merge branch 'dev' into Add-wait-for-subscribers-feature
Arkatufus May 16, 2025
8083046
Merge branch 'dev' into Add-wait-for-subscribers-feature
Arkatufus May 16, 2025
c0af9fe
Trim features to fit
Arkatufus May 16, 2025
89ecf1d
Merge branch 'Add-wait-for-subscribers-feature' of github.com:Arkatuf…
Arkatufus May 16, 2025
b4b1c5e
Simplify logic
Arkatufus May 19, 2025
00761f5
Code cleanup
Arkatufus May 19, 2025
6cf2e00
Code cleanup
Arkatufus May 19, 2025
4c0f026
Update API Approval list
Arkatufus May 19, 2025
6792915
Fix code
Arkatufus May 19, 2025
cba4f30
Fix codes
Arkatufus May 19, 2025
13d05ca
Fix codes
Arkatufus May 19, 2025
6f26d41
Update code
Arkatufus May 19, 2025
ef05c31
Update API Approval list
Arkatufus May 19, 2025
1dbdd89
Fix PublishWithAck
Arkatufus May 19, 2025
8a36742
Fix cleanup code
Arkatufus May 19, 2025
71d4dd2
refactor DistributedPubSubSettings to record
Arkatufus May 19, 2025
ec979e2
Fix specs
Arkatufus May 19, 2025
8635d6c
Add unit tests
Arkatufus May 19, 2025
2021f83
Fix unit tests
Arkatufus May 19, 2025
4d2130b
Merge branch 'dev' into Add-wait-for-subscribers-feature
Arkatufus May 19, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ public override string ToString()
/// TBD
/// </summary>
[Serializable]
public sealed class Publish : IDistributedPubSubMessage, IEquatable<Publish>, IWrappedMessage
public class Publish : IDistributedPubSubMessage, IEquatable<Publish>, IWrappedMessage
{
/// <summary>
/// TBD
Expand Down Expand Up @@ -415,6 +415,20 @@ public override string ToString()
return $"Publish<topic:{Topic}, sendOneToEachGroup:{SendOneMessageToEachGroup}, message:{Message}>";
}
}

public sealed class PublishWithAck : Publish
Comment thread
Arkatufus marked this conversation as resolved.
Outdated
{
public PublishWithAck(string topic, object message, TimeSpan timeout, bool sendOneMessageToEachGroup = false) : base(topic, message, sendOneMessageToEachGroup)
{
Timeout = timeout;
}

public TimeSpan Timeout { get; }
}

public sealed record PublishFailed(PublishWithAck Message);
Comment thread
Arkatufus marked this conversation as resolved.
Outdated

public sealed record PublishSucceeded(PublishWithAck Message);
Comment thread
Arkatufus marked this conversation as resolved.
Outdated

/// <summary>
/// TBD
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Akka.Cluster.Tools.PublishSubscribe.Internal;
using Akka.Event;
using Akka.Pattern;
using Akka.Remote;
using Akka.Routing;
using Akka.Util;
using Status = Akka.Cluster.Tools.PublishSubscribe.Internal.Status;
Expand Down Expand Up @@ -102,6 +103,7 @@ public class DistributedPubSubMediator : ReceiveActor, IWithTimers
{
private const string GossipTimerKey = "GossipTimer";
private const string PruneTimerKey = "PruneTimer";
private const string PruneBufferTimerKey = "PruneBufferTimer";

/// <summary>
/// TBD
Expand All @@ -127,20 +129,19 @@ public static Props Props(DistributedPubSubSettings settings)
private readonly string _topicPrefix;
private readonly PubSubCache _cache;

private readonly int _maxBufferPerTopic;
private readonly TimeSpan _bufferedMessageTimeoutCheckInterval;
private readonly Dictionary<string, List<BufferedMessage>> _bufferedMessages = new();
Comment thread
Arkatufus marked this conversation as resolved.

public ITimerScheduler Timers { get; set; }

/// <summary>
/// TBD
/// </summary>
public IImmutableDictionary<Address, long> OwnVersions
{
get
{
return _registry
.Select(entry => new KeyValuePair<Address, long>(entry.Key, entry.Value.Version))
.ToImmutableDictionary(kv => kv.Key, kv => kv.Value);
}
}
=> _registry

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No logic change, just modernization

.Select(entry => new KeyValuePair<Address, long>(entry.Key, entry.Value.Version))
.ToImmutableDictionary(kv => kv.Key, kv => kv.Value);

/// <summary>
/// TBD
Expand All @@ -161,6 +162,9 @@ public DistributedPubSubMediator(DistributedPubSubSettings settings)
_log = Context.GetLogger();

_role = settings.Role;
_maxBufferPerTopic = settings.MaxBufferedMessagePerTopic;
_bufferedMessageTimeoutCheckInterval = settings.BufferedMessageTimeoutCheckInterval;

_pruneInterval = new TimeSpan(_settings.RemovedTimeToLive.Ticks / 2);
_buffer = new PerGroupingBuffer();

Expand Down Expand Up @@ -195,7 +199,7 @@ public DistributedPubSubMediator(DistributedPubSubSettings settings)
new Router(_settings.RoutingLogic, routees.ToArray()).Route(
Internal.Utils.WrapIfNeeded(send.Message), Sender);
else
IgnoreOrSendToDeadLetters(send);
IgnoreOrSendToDeadLetters(send, Sender);
});
Receive<SendToAll>(sendToAll =>
{
Expand Down Expand Up @@ -320,14 +324,54 @@ public DistributedPubSubMediator(DistributedPubSubSettings settings)
if (!_registry.TryGetValue(bucket.Owner, out var myBucket))
myBucket = new Bucket(bucket.Owner);

if (bucket.Version > myBucket.Version)
_registry[bucket.Owner] = new Bucket(myBucket.Owner, bucket.Version, myBucket.Content.SetItems(bucket.Content));
if (bucket.Version <= myBucket.Version)
continue;

// Create a bucket diff, we're only interested in new items being added
var keys = new HashSet<string>(bucket.Content.Keys);
keys.ExceptWith(new HashSet<string>(myBucket.Content.Keys));
if (keys.Count > 0)
{
// Send the diff to ourselves
Self.Tell(new NewBucketKeysAdded(keys));
}
Comment on lines +331 to +353

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Capture Delta diff, we're interested in new keys being propagated to this cluster node from other nodes.


// Merge remote bucket with ours
var newBucket = new Bucket(myBucket.Owner, bucket.Version, myBucket.Content.SetItems(bucket.Content));
_registry[bucket.Owner] = newBucket;
Comment on lines +356 to +357

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Old code, no logic change

}
}
}
});
Receive<GossipTick>(_ => HandleGossip());
Receive<Prune>(_ => HandlePrune());
Receive<NewBucketKeysAdded>(subs =>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

New message handler, this handles new keys being added into the registry bucket

{
foreach (var key in subs.Topics)
{
if (!_bufferedMessages.TryGetValue(key, out var buffer))
continue;

foreach (var bufferedMessage in buffer)
{
Self.Tell(bufferedMessage.Message, bufferedMessage.Sender);
}

_bufferedMessages.Remove(key);
}
Comment on lines +366 to +377

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For each newly added keys, see if we have a matching buffer. If we do, resend all of them

});
Receive<PruneBufferTick>(_ =>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

New message handler for timed-out message pruning

{
foreach (var buffer in _bufferedMessages.Values)
{
var removed = buffer.Where(bufferedMessage => bufferedMessage.Deadline.IsOverdue).ToArray();
foreach (var removedMsg in removed)
{
buffer.Remove(removedMsg);
IgnoreOrSendToDeadLetters(removedMsg.Message, removedMsg.Sender);
}
}
Comment thread
Aaronontheweb marked this conversation as resolved.
});
Receive<Terminated>(terminated =>
{
var key = Internal.Utils.MakeKey(terminated.ActorRef);
Expand Down Expand Up @@ -417,6 +461,32 @@ public DistributedPubSubMediator(DistributedPubSubSettings settings)
});
}

private void BufferMessageOrDeadLetter(PublishWithAck message, IActorRef sender)
{
var topic = message.Topic;
var encodedTopic = _cache.EncodeName(topic);
var key = _cache.MakeKey(Self.Path, encodedTopic);
Comment on lines +481 to +483

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Generate the key for the topic


if (!_bufferedMessages.TryGetValue(key, out var buffer))
{
buffer = [];
_bufferedMessages[key] = buffer;
}

if(buffer.Count >= _maxBufferPerTopic)
{
_log.Warning("PublishWithAck buffer overflowed for topic [{0}]. New message inserted: {1}", topic, message);
while (buffer.Count >= _maxBufferPerTopic)
{
var removed = buffer[0];
buffer.RemoveAt(0);
IgnoreOrSendToDeadLetters(removed.Message, removed.Sender);
}
}
Comment thread
Aaronontheweb marked this conversation as resolved.

buffer.Add(new BufferedMessage(message, new Deadline(DateTime.UtcNow + message.Timeout), sender));
Comment thread
Arkatufus marked this conversation as resolved.
}

private bool OtherHasNewerVersions(IImmutableDictionary<Address, long> versions)
{
return versions.Any(entry =>
Expand All @@ -438,11 +508,8 @@ private IEnumerable<Bucket> CollectDelta(IImmutableDictionary<Address, long> ver
}

var count = 0;
foreach (var entry in filledOtherVersions)
foreach (var (owner, v) in filledOtherVersions)
{
var owner = entry.Key;
var v = entry.Value;

if (!_registry.TryGetValue(owner, out var bucket))
bucket = new Bucket(owner);

Expand Down Expand Up @@ -493,15 +560,21 @@ private void HandleRegisterTopic(IActorRef actorRef)
private void PutToRegistry(string key, IActorRef value)
{
var v = NextVersion();
if (!_registry.TryGetValue(_cluster.SelfAddress, out var bucket))
_registry.Add(_cluster.SelfAddress,
new Bucket(_cluster.SelfAddress, v, ImmutableDictionary<string, ValueHolder>.Empty.Add(key, new ValueHolder(v, value))));
else
_registry[_cluster.SelfAddress] = new Bucket(bucket.Owner, v, bucket.Content.SetItem(key, new ValueHolder(v, value)));
var newBucket = _registry.TryGetValue(_cluster.SelfAddress, out var bucket)
? new Bucket(bucket.Owner, v, bucket.Content.SetItem(key, new ValueHolder(v, value)))
: new Bucket(_cluster.SelfAddress, v, ImmutableDictionary<string, ValueHolder>.Empty.Add(key, new ValueHolder(v, value)));

_registry[_cluster.SelfAddress] = newBucket;
Comment on lines +578 to +582

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Old code, no logic changed

Self.Tell(new NewBucketKeysAdded([key]));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Send a signal to self that a new key has been added

}

private void IgnoreOrSendToDeadLetters(IWrappedMessage message)
private void IgnoreOrSendToDeadLetters(object message, IActorRef sender)
{
if (message is PublishWithAck needAck)
{
sender.Tell(new PublishFailed(needAck));
}
Comment on lines +573 to +594

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If the message is PublishWithAck, notify sender that it failed


if (_settings.SendToDeadLettersWhenNoSubscribers)
{
var topic = message switch
Expand All @@ -514,7 +587,7 @@ private void IgnoreOrSendToDeadLetters(IWrappedMessage message)
// Use the specialized DeadLetterWithNoSubscribers class to clearly indicate
// that the message was not delivered because there were no subscribers,
// not because the mediator itself is dead.
var deadLetter = new DeadLetterWithNoSubscribers(message, topic, Sender, Context.Self);
var deadLetter = new DeadLetterWithNoSubscribers(message, topic, sender, Context.Self);
Context.System.DeadLetters.Tell(deadLetter);
}
}
Expand All @@ -527,9 +600,17 @@ private void PublishMessage(string path, IWrappedMessage publish, bool allButSel
if (r == null) continue;
r.Forward(publish.Message);
counter++;
if(publish is PublishWithAck needAck)
Sender.Tell(new PublishSucceeded(needAck));
Comment thread
Arkatufus marked this conversation as resolved.
Outdated
}

if (counter == 0) IgnoreOrSendToDeadLetters(publish);
if (counter == 0)
{
if(publish is PublishWithAck needAck)
BufferMessageOrDeadLetter(needAck, Sender);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Buffer message if its a PublishWithAck

else
IgnoreOrSendToDeadLetters(publish, Sender);
}
return;

IEnumerable<IActorRef> Refs()
Expand All @@ -555,7 +636,10 @@ private void PublishToEachGroup(string path, Publish publish)

if (groups.Count == 0)
{
IgnoreOrSendToDeadLetters(publish);
if(publish is PublishWithAck needAck)
BufferMessageOrDeadLetter(needAck, Sender);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Buffer message if its a PublishWithAck

else
IgnoreOrSendToDeadLetters(publish, Sender);
}
else
{
Expand All @@ -565,6 +649,9 @@ private void PublishToEachGroup(string path, Publish publish)
if (routees.Length != 0)
new Router(_settings.RoutingLogic, routees).Route(wrappedMessage, Sender);
}

if(publish is PublishWithAck needAck)
Sender.Tell(new PublishSucceeded(needAck));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If message is PublishWithAck, send a success signal to the sender

}
}

Expand All @@ -582,8 +669,8 @@ private IEnumerable<KeyValuePair<string, Routee>> ExtractGroups(string prefix, s

private void HandlePrune()
{
var modifications = new Dictionary<Address, Bucket>();
foreach (var (owner, bucket) in _registry)
var modifications = new List<Bucket>();
foreach (var bucket in _registry.Values)
Comment on lines +692 to +693

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Simplify logic, the bucket itself already stores the bucker owner, there is no need to scan the dictionary keys.

{
var oldRemoved = bucket.Content
.Where(kv => kv.Value.Ref.IsNobody() && (bucket.Version - kv.Value.Version) > _settings.RemovedTimeToLive.TotalMilliseconds)
Expand All @@ -592,13 +679,13 @@ private void HandlePrune()

if (oldRemoved.Length > 0)
{
modifications.Add(owner, new Bucket(bucket.Owner, bucket.Version, bucket.Content.RemoveRange(oldRemoved)));
modifications.Add(new Bucket(bucket.Owner, bucket.Version, bucket.Content.RemoveRange(oldRemoved)));
}
}

foreach (var entry in modifications)
{
_registry[entry.Key] = entry.Value;
_registry[entry.Owner] = entry;
}
}

Expand Down Expand Up @@ -634,6 +721,7 @@ protected override void PreStart()
//Start periodic gossip to random nodes in cluster
Timers.StartPeriodicTimer(GossipTimerKey, GossipTick.Instance, _settings.GossipInterval, _settings.GossipInterval, Self);
Timers.StartPeriodicTimer(PruneTimerKey, Prune.Instance, _pruneInterval, _pruneInterval, Self);
Timers.StartPeriodicTimer(PruneBufferTimerKey, PruneBufferTick.Instance, _bufferedMessageTimeoutCheckInterval, _bufferedMessageTimeoutCheckInterval, Self);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Start timed out message pruning timer

}

/// <summary>
Expand Down
Loading