Skip to content

Commit f9475b5

Browse files
DistributedPubSub: clearer logging when DeadLetter publishing due to no subscribers (#7646)
* Fix #7626: Using DeadLetterWithNoSubscribers to clearly indicate when there are no subscribers * Fixed #7626: Using internal DeadLetterWithNoSubscribers class to clearly indicate no subscriber case in dead letter messages - Made DeadLetterWithNoSubscribers internal (class already existed) - Added test to verify dead letter messages are sent when no subscribers exist * Update AGENT.md with additional TestKit guidance for ITestOutputHelper * fix `DistributedPubSubDeadLetterSpec` * added topic logging * be consistent about nullables * fix compilation issue --------- Co-authored-by: Gregorius Soedharmo <[email protected]>
1 parent e0cc0c4 commit f9475b5

File tree

5 files changed

+184
-4
lines changed

5 files changed

+184
-4
lines changed

AGENT.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Akka.NET Agent Guidelines
2+
3+
## Build/Test Commands
4+
- Build solution: `dotnet build`
5+
- Build with warnings as errors: `dotnet build -warnaserror`
6+
- Run all tests: `dotnet test -c Release`
7+
- Run specific test: `dotnet test -c Release --filter DisplayName="TestName"` or `dotnet test path/to/project.csproj`
8+
- Format check: `dotnet format --verify-no-changes`
9+
10+
## Git Repository Management
11+
- Setup remotes:
12+
- `git remote add upstream https://github.com/akkadotnet/akka.net.git` (main repository)
13+
- `git remote add origin https://github.com/yourusername/akka.net.git` (your fork)
14+
- Sync with upstream:
15+
- `git fetch upstream` (get latest changes from main repo)
16+
- `git checkout dev` (switch to dev branch)
17+
- `git merge upstream/dev` (merge changes from upstream)
18+
- `git push origin dev` (update your fork)
19+
- Create feature branch:
20+
- `git checkout -b feature/your-feature-name` (create and switch to new branch)
21+
- `git push -u origin feature/your-feature-name` (push branch to your fork)
22+
23+
## Code Style Guidelines
24+
- Use Allman style brackets for C# code (opening brace on new line)
25+
- 4 spaces for indentation
26+
- Prefer "var" everywhere when type is apparent
27+
- Private fields start with `_` (underscore), PascalCase for public/protected members
28+
- No "this." qualifier when unnecessary
29+
- Use exceptions for error handling (IllegalStateException for invalid states)
30+
- Sort using statements with System.* appearing first
31+
- XML comments for public APIs
32+
- Name tests with descriptive `DisplayName=` attributes
33+
- Default to `sealed` classes and records for data objects
34+
- Enable nullability in new/modified files with `#nullable enable`
35+
- Never use `async void`, `.Result`, or `.Wait()` - these cause deadlocks
36+
- Always pass `CancellationToken` in async methods
37+
38+
## API Approvals
39+
- Run API approval tests when making public API changes: `dotnet test -c Release src/core/Akka.API.Tests`
40+
- Approval files are located at `src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt`
41+
- Install a diff viewer like WinMerge or TortoiseMerge to approve API changes
42+
- Follow extend-only design principles - don't modify existing public APIs, only extend them
43+
- Mark deprecated APIs with `[Obsolete("Obsolete since v{current-akka-version}")]`
44+
45+
## Conventions
46+
- Stay close to JVM Akka where applicable but be .NET idiomatic
47+
- Use Task<T> instead of Future, TimeSpan instead of Duration
48+
- Include unit tests with changes
49+
- Preserve public API and wire compatibility
50+
- Keep pull requests small and focused (<300 lines when possible)
51+
- Fix warnings instead of suppressing them
52+
- Treat TBD comments as action items to be resolved
53+
- Benchmark performance-critical code changes with BenchmarkDotNet
54+
- Avoid adding new dependencies without license/security checks
55+
56+
## Akka.NET TestKit Guidelines
57+
- Actor tests should derive from `AkkaSpec` or `TestKit` to access actor testing facilities
58+
- Pass `ITestOutputHelper output` to the constructor and base constructor: `public MySpec(ITestOutputHelper output) : base(config, output)`
59+
- Use the `ITestOutputHelper` output for debugging: it captures all test output including actor system logs
60+
- Configure proper logging in tests: `akka.loglevel = DEBUG` or `akka.loglevel = INFO`
61+
- Use `EventFilter` to assert on log messages (e.g., `EventFilter.Error().ExpectOne(() => { /* test code */ });`)
62+
- For testing deadletters, use `EventFilter.DeadLetter().Expect(1, () => { /* code that should produce dead letter */ });`
63+
- Test message assertions using `ExpectMsg<T>()`, `ExpectNoMsg()`, or `FishForMessage<T>()`
64+
- Set explicit timeouts for message expectations to avoid long-running tests
65+
- Use `TestProbe` to create lightweight test actors to verify interactions
66+
- Tests should clean up after themselves (stop created actors, reset state)
67+
- To test specialized message types, verify the type wrapper in logs: `wrapped in [$TypeName]`
68+
69+
## Repository Landmarks
70+
- `src/` - All runtime / library code
71+
- `src/benchmark/` - Micro-benchmarks (BenchmarkDotNet)
72+
- `src/…Tests/` - xUnit test projects
73+
- `docs/community/contributing/` - Contributor policies & style guides
74+
- `docs/` - Public facing documentation
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//-----------------------------------------------------------------------
2+
// <copyright file="DistributedPubSubDeadLetterSpec.cs" company="Akka.NET Project">
3+
// Copyright (C) 2009-2022 Lightbend Inc. <http://www.lightbend.com>
4+
// Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
5+
// </copyright>
6+
//-----------------------------------------------------------------------
7+
8+
using System.Threading.Tasks;
9+
using Akka.Actor;
10+
using Akka.Cluster.Tools.PublishSubscribe;
11+
using Akka.Configuration;
12+
using Akka.Event;
13+
using Akka.TestKit;
14+
using Xunit;
15+
using Xunit.Abstractions;
16+
17+
namespace Akka.Cluster.Tools.Tests.PublishSubscribe
18+
{
19+
public class DistributedPubSubDeadLetterSpec : AkkaSpec
20+
{
21+
public DistributedPubSubDeadLetterSpec(ITestOutputHelper output) : base(GetConfig(), output)
22+
{
23+
}
24+
25+
public static Config GetConfig()
26+
{
27+
return ConfigurationFactory.ParseString(
28+
@"akka.actor.provider = cluster"
29+
+ "\nakka.loglevel = INFO"
30+
+ "\nakka.log-dead-letters = on");
31+
}
32+
33+
[Fact]
34+
public async Task DistributedPubSubMediator_should_send_specialized_dead_letter_message_when_no_subscribers()
35+
{
36+
// arrange
37+
var mediator = DistributedPubSub.Get(Sys).Mediator;
38+
var testMessage = "test-message";
39+
40+
// act - publish to a topic that no one is subscribed to
41+
await EventFilter.Info(contains: "DeadLetterWithNoSubscribers")
42+
.ExpectAsync(1, () =>
43+
{
44+
mediator.Tell(new Publish("unused-topic", testMessage));
45+
return Task.CompletedTask;
46+
});
47+
}
48+
}
49+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//-----------------------------------------------------------------------
2+
// <copyright file="DeadLetterWithNoSubscribers.cs" company="Akka.NET Project">
3+
// Copyright (C) 2009-2022 Lightbend Inc. <http://www.lightbend.com>
4+
// Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
5+
// </copyright>
6+
//-----------------------------------------------------------------------
7+
8+
using Akka.Actor;
9+
using Akka.Event;
10+
11+
namespace Akka.Cluster.Tools.PublishSubscribe
12+
{
13+
/// <summary>
14+
/// Special case of Dead Letter that explicitly indicates the message was sent to
15+
/// DeadLetters because there were no subscribers for the topic in DistributedPubSub,
16+
/// NOT because the mediator itself is dead.
17+
/// </summary>
18+
internal sealed class DeadLetterWithNoSubscribers : AllDeadLetters
19+
{
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="DeadLetterWithNoSubscribers"/> class.
22+
/// </summary>
23+
/// <param name="message">The original message that could not be delivered.</param>
24+
/// <param name="topic">The topic that the message was sent to.</param>
25+
/// <param name="sender">The actor that sent the message.</param>
26+
/// <param name="recipient">The actor that was to receive the message (usually the mediator itself).</param>
27+
public DeadLetterWithNoSubscribers(object message, string? topic, IActorRef sender, IActorRef recipient)
28+
: base(message, sender, recipient)
29+
{
30+
Topic = topic;
31+
}
32+
33+
public string? Topic { get; }
34+
35+
/// <summary>
36+
/// Returns a string that represents the current object.
37+
/// </summary>
38+
/// <returns>A string that represents the current object.</returns>
39+
public override string ToString()
40+
{
41+
return $"DeadLetterWithNoSubscribers from {Sender} to {Recipient}: <{Message}> - No subscribers found for topic {Topic}";
42+
}
43+
}
44+
}

src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/DistributedPubSubMediator.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//-----------------------------------------------------------------------
1+
//-----------------------------------------------------------------------
22
// <copyright file="DistributedPubSubMediator.cs" company="Akka.NET Project">
33
// Copyright (C) 2009-2022 Lightbend Inc. <http://www.lightbend.com>
44
// Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
@@ -500,10 +500,23 @@ private void PutToRegistry(string key, IActorRef value)
500500
_registry[_cluster.SelfAddress] = new Bucket(bucket.Owner, v, bucket.Content.SetItem(key, new ValueHolder(v, value)));
501501
}
502502

503-
private void IgnoreOrSendToDeadLetters(object message)
503+
private void IgnoreOrSendToDeadLetters(IWrappedMessage message)
504504
{
505505
if (_settings.SendToDeadLettersWhenNoSubscribers)
506-
Context.System.DeadLetters.Tell(new DeadLetter(message, Sender, Context.Self));
506+
{
507+
var topic = message switch
508+
{
509+
Publish publish => publish.Topic,
510+
Send send => $"Send:{send.Path}",
511+
_ => null
512+
};
513+
514+
// Use the specialized DeadLetterWithNoSubscribers class to clearly indicate
515+
// that the message was not delivered because there were no subscribers,
516+
// not because the mediator itself is dead.
517+
var deadLetter = new DeadLetterWithNoSubscribers(message, topic, Sender, Context.Self);
518+
Context.System.DeadLetters.Tell(deadLetter);
519+
}
507520
}
508521

509522
private void PublishMessage(string path, IWrappedMessage publish, bool allButSelf = false)

src/core/Akka/Event/DeadLetterListener.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//-----------------------------------------------------------------------
1+
//-----------------------------------------------------------------------
22
// <copyright file="DeadLetterListener.cs" company="Akka.NET Project">
33
// Copyright (C) 2009-2022 Lightbend Inc. <http://www.lightbend.com>
44
// Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>

0 commit comments

Comments
 (0)