Skip to content
Merged
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
3 changes: 3 additions & 0 deletions src/Persistence/MySql/Wolverine.MySql/AssemblyAttributes.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Runtime.CompilerServices;
using Wolverine.Attributes;

[assembly: ExcludeFromServiceCapabilities]

[assembly: InternalsVisibleTo("MySqlTests")]
3 changes: 3 additions & 0 deletions src/Persistence/Oracle/Wolverine.Oracle/AssemblyAttributes.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Runtime.CompilerServices;
using Wolverine.Attributes;

[assembly: ExcludeFromServiceCapabilities]

[assembly: InternalsVisibleTo("OracleTests")]
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Runtime.CompilerServices;
using Wolverine.Attributes;

[assembly: ExcludeFromServiceCapabilities]

[assembly: InternalsVisibleTo("PersistenceTests")]
[assembly: InternalsVisibleTo("EfCoreTests")]
Expand Down
3 changes: 3 additions & 0 deletions src/Persistence/Wolverine.Marten/AssemblyAttributes.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Runtime.CompilerServices;
using Wolverine.Attributes;

[assembly: ExcludeFromServiceCapabilities]

[assembly: InternalsVisibleTo("PersistenceTests")]
[assembly: InternalsVisibleTo("MartenTests")]
Expand Down
3 changes: 3 additions & 0 deletions src/Persistence/Wolverine.Polecat/AssemblyAttributes.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Runtime.CompilerServices;
using Wolverine.Attributes;

[assembly: ExcludeFromServiceCapabilities]

[assembly: InternalsVisibleTo("PolecatTests")]
[assembly: InternalsVisibleTo("Wolverine.Http.Polecat")]
3 changes: 3 additions & 0 deletions src/Persistence/Wolverine.Postgresql/AssemblyAttributes.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Runtime.CompilerServices;
using Wolverine.Attributes;

[assembly: ExcludeFromServiceCapabilities]

[assembly: InternalsVisibleTo("Wolverine.Marten")]
[assembly: InternalsVisibleTo("PersistenceTests")]
Expand Down
3 changes: 3 additions & 0 deletions src/Persistence/Wolverine.RDBMS/AssemblyAttributes.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Runtime.CompilerServices;
using Wolverine.Attributes;

[assembly: ExcludeFromServiceCapabilities]

[assembly: InternalsVisibleTo("PersistenceTests")]
[assembly: InternalsVisibleTo("SqlServerTests")]
Expand Down
3 changes: 3 additions & 0 deletions src/Persistence/Wolverine.SqlServer/AssemblyAttributes.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Runtime.CompilerServices;
using Wolverine.Attributes;

[assembly: ExcludeFromServiceCapabilities]

[assembly: InternalsVisibleTo("PersistenceTests")]
[assembly: InternalsVisibleTo("SqlServerTests")]
3 changes: 3 additions & 0 deletions src/Persistence/Wolverine.Sqlite/AssemblyAttributes.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Runtime.CompilerServices;
using Wolverine.Attributes;

[assembly: ExcludeFromServiceCapabilities]

[assembly: InternalsVisibleTo("SqliteTests")]
152 changes: 152 additions & 0 deletions src/Testing/CoreTests/Acceptance/system_message_type_filtering.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using Microsoft.Extensions.Hosting;
using Wolverine.Configuration;
using Wolverine.Configuration.Capabilities;
using Wolverine.ErrorHandling;
using Wolverine.Logging;
using Wolverine.Runtime;
using Wolverine.Runtime.Agents;
using Wolverine.Runtime.Metrics;
using Wolverine.Runtime.Routing;
using Wolverine.Tracking;
using Wolverine.Transports;
using Xunit;

namespace CoreTests.Acceptance;

/// <summary>
/// GH-2520: framework-internal message types must not leak into ServiceCapabilities
/// or be reported via IWolverineObserver hooks (MessageRouted / MessageCausedBy).
/// </summary>
public class system_message_type_filtering
{
[Fact]
public void IsSystemMessageType_recognizes_IInternalMessage()
{
typeof(SampleInternalMessage).IsSystemMessageType().ShouldBeTrue();
}

[Fact]
public void IsSystemMessageType_recognizes_IAgentCommand()
{
typeof(SampleAgentCommand).IsSystemMessageType().ShouldBeTrue();
}

[Fact]
public void IsSystemMessageType_recognizes_INotToBeRouted()
{
typeof(SampleNotToBeRouted).IsSystemMessageType().ShouldBeTrue();
}

[Fact]
public void IsSystemMessageType_returns_false_for_normal_user_messages()
{
typeof(NormalUserMessage).IsSystemMessageType().ShouldBeFalse();
}

[Fact]
public void IsSystemMessageType_returns_false_for_null()
{
((Type?)null).IsSystemMessageType().ShouldBeFalse();
}

[Fact]
public async Task service_capabilities_excludes_system_message_types()
{
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Discovery.DisableConventionalDiscovery();
opts.Discovery.IncludeType<SystemFilteringHandler>();
})
.StartAsync();

var capabilities = await ServiceCapabilities.ReadFrom(host.GetRuntime(), null, CancellationToken.None);

// Sanity: the normal user message should appear
capabilities.Messages.ShouldContain(m => m.Type.FullName == typeof(NormalUserMessage).FullName);

// The three system-marked types must NOT appear
capabilities.Messages.ShouldNotContain(m => m.Type.FullName == typeof(SampleInternalMessage).FullName);
capabilities.Messages.ShouldNotContain(m => m.Type.FullName == typeof(SampleAgentCommand).FullName);
capabilities.Messages.ShouldNotContain(m => m.Type.FullName == typeof(SampleNotToBeRouted).FullName);
}

[Fact]
public async Task observer_does_not_receive_message_routed_for_system_types()
{
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Discovery.DisableConventionalDiscovery();
opts.Discovery.IncludeType<SystemFilteringHandler>();
})
.StartAsync();

var runtime = host.GetRuntime();

// Swap in a recording observer after startup so we capture exactly the
// post-startup MessageRouted calls we care about.
var observer = new RecordingObserver();
runtime.Observer = observer;

// Force routing for each type so the observer hook is exercised
runtime.RoutingFor(typeof(NormalUserMessage));
runtime.RoutingFor(typeof(SampleInternalMessage));
runtime.RoutingFor(typeof(SampleAgentCommand));
runtime.RoutingFor(typeof(SampleNotToBeRouted));

// Normal type should be reported, system types should be filtered out
observer.RoutedTypes.ShouldContain(typeof(NormalUserMessage));
observer.RoutedTypes.ShouldNotContain(typeof(SampleInternalMessage));
observer.RoutedTypes.ShouldNotContain(typeof(SampleAgentCommand));
observer.RoutedTypes.ShouldNotContain(typeof(SampleNotToBeRouted));
}
}

// Fixtures live in the test (user) assembly to prove the per-type filter works
// regardless of which assembly declares the type.
public record NormalUserMessage(string Name);
public record SampleInternalMessage(string Note) : IInternalMessage;
public record SampleNotToBeRouted(string Note) : INotToBeRouted;

public class SampleAgentCommand : IAgentCommand
{
public Task<AgentCommands> ExecuteAsync(IWolverineRuntime runtime, CancellationToken cancellationToken)
=> Task.FromResult(AgentCommands.Empty);
}

public class SystemFilteringHandler
{
public void Handle(NormalUserMessage msg) { }
public void Handle(SampleInternalMessage msg) { }
public void Handle(SampleNotToBeRouted msg) { }
public void Handle(SampleAgentCommand cmd) { }
}

internal class RecordingObserver : IWolverineObserver
{
public List<Type> RoutedTypes { get; } = new();

public void MessageRouted(Type messageType, IMessageRouter router)
{
RoutedTypes.Add(messageType);
}

// All other members are no-ops (interface uses default implementations)
public Task AssumedLeadership() => Task.CompletedTask;
public Task NodeStarted() => Task.CompletedTask;
public Task NodeStopped() => Task.CompletedTask;
public Task AgentStarted(Uri agentUri) => Task.CompletedTask;
public Task AgentStopped(Uri agentUri) => Task.CompletedTask;
public Task AssignmentsChanged(AssignmentGrid grid, AgentCommands commands) => Task.CompletedTask;
public Task StaleNodes(IReadOnlyList<WolverineNode> staleNodes) => Task.CompletedTask;
public Task RuntimeIsFullyStarted() => Task.CompletedTask;
public void EndpointAdded(Endpoint endpoint) { }
public Task BackPressureTriggered(Endpoint endpoint, IListeningAgent agent) => Task.CompletedTask;
public Task BackPressureLifted(Endpoint endpoint) => Task.CompletedTask;
public Task ListenerLatched(Endpoint endpoint) => Task.CompletedTask;
public Task CircuitBreakerTripped(Endpoint endpoint, CircuitBreakerOptions options) => Task.CompletedTask;
public Task CircuitBreakerReset(Endpoint endpoint) => Task.CompletedTask;
public void PersistedCounts(Uri storeUri, PersistedCounts counts) { }
public void MessageHandlingMetricsExported(MessageHandlingMetrics metrics) { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Text.Json.Serialization;
using JasperFx.Core.Reflection;
using JasperFx.Descriptors;
using Wolverine.Attributes;
using JasperFx.Events;
using JasperFx.Events.Descriptors;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -107,7 +106,7 @@ private static void readMessageTypes(IWolverineRuntime runtime, ServiceCapabilit
var messageTypes = runtime.Options.Discovery.FindAllMessages(runtime.Options.HandlerGraph);
foreach (var messageType in messageTypes.OrderBy(x => x.FullNameInCode()))
{
if (messageType.Assembly.HasAttribute<ExcludeFromServiceCapabilitiesAttribute>()) continue;
if (messageType.IsSystemMessageType()) continue;
capabilities.Messages.Add(new MessageDescriptor(messageType, runtime));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using JasperFx.Core.Reflection;
using Wolverine.Attributes;
using Wolverine.Runtime;
using Wolverine.Runtime.Agents;

namespace Wolverine.Configuration.Capabilities;

/// <summary>
/// Centralized predicate for deciding whether a given message type is a system /
/// framework-internal type that should be excluded from observability surfaces:
/// <see cref="ServiceCapabilities"/>, <c>IWolverineObserver.MessageRouted</c>, and
/// <c>IWolverineObserver.MessageCausedBy</c>.
/// </summary>
internal static class SystemMessageTypeExtensions
{
/// <summary>
/// Returns true if the supplied message type should be hidden from observability.
/// Catches:
/// <list type="bullet">
/// <item>Types implementing <see cref="IInternalMessage"/> (preferred — fastest)</item>
/// <item>Types implementing <see cref="IAgentCommand"/> (Wolverine internal commands)</item>
/// <item>Types implementing <see cref="INotToBeRouted"/> (covers <see cref="ISideEffect"/>,
/// <c>ICritterWatchMessage</c>, acknowledgements, etc.)</item>
/// <item>Types declared in an assembly marked with <see cref="ExcludeFromServiceCapabilitiesAttribute"/></item>
/// </list>
/// </summary>
public static bool IsSystemMessageType(this Type? messageType)
{
if (messageType is null) return false;

// Marker-interface checks first — these are constant-time runtime type tests.
if (messageType.CanBeCastTo<IInternalMessage>()) return true;
if (messageType.CanBeCastTo<IAgentCommand>()) return true;
if (messageType.CanBeCastTo<INotToBeRouted>()) return true;

// Assembly-level opt-out — slower, falls through to reflection but cached by the runtime.
if (messageType.Assembly.HasAttribute<ExcludeFromServiceCapabilitiesAttribute>()) return true;

return false;
}
}
16 changes: 15 additions & 1 deletion src/Wolverine/Runtime/Handlers/MessageHandler.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using JasperFx.Core.Reflection;
using Microsoft.Extensions.Logging;
using Wolverine.Configuration.Capabilities;
using Wolverine.Runtime.Agents;

namespace Wolverine.Runtime.Handlers;
Expand Down Expand Up @@ -59,13 +60,26 @@ public void RecordCauseAndEffect(MessageContext context, IWolverineObserver obse
{
if (!context.Runtime.Options.EnableMessageCausationTracking) return;

// Skip the entire causation report when the incoming message itself is a
// framework-internal type (IAgentCommand, INotToBeRouted, IInternalMessage, etc.).
// See GH-2520.
if (MessageType.IsSystemMessageType()) return;

var incomingType = MessageType.FullName ?? MessageType.Name;
var handlerType = GetType().FullName ?? GetType().Name;
var endpointUri = Chain?.Endpoints?.FirstOrDefault()?.Uri?.ToString();

foreach (var envelope in context.Outstanding)
{
var outgoingType = envelope.Message?.GetType().FullName;
var outgoingMessage = envelope.Message;
if (outgoingMessage is null) continue;

// Per-instance check uses fast pattern match over runtime type-tests for
// the marker interfaces; falls through to the helper for assembly attrs.
var outgoingMessageType = outgoingMessage.GetType();
if (outgoingMessageType.IsSystemMessageType()) continue;

var outgoingType = outgoingMessageType.FullName;
if (string.IsNullOrEmpty(outgoingType)) continue;

var key = $"{incomingType}->{outgoingType}@{handlerType}";
Expand Down
15 changes: 15 additions & 0 deletions src/Wolverine/Runtime/IInternalMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Wolverine.Runtime;

/// <summary>
/// Marker interface for message types that are internal to Wolverine or to extension
/// frameworks (such as CritterWatch). Types implementing this interface are excluded
/// from <see cref="Wolverine.Configuration.Capabilities.ServiceCapabilities"/> and
/// from <c>IWolverineObserver</c> notifications such as <c>MessageRouted</c> and
/// <c>MessageCausedBy</c>.
///
/// Use this for system commands and infrastructure messages that should not appear in
/// observability tooling alongside user-defined messages. Per-instance filtering uses
/// a single type-test (<c>x is IInternalMessage</c>), which is cheaper than reflection
/// or attribute lookups.
/// </summary>
public interface IInternalMessage;
9 changes: 8 additions & 1 deletion src/Wolverine/Runtime/WolverineRuntime.Routing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using JasperFx.Core;
using JasperFx.Core.Reflection;
using Wolverine.Configuration;
using Wolverine.Configuration.Capabilities;
using Wolverine.Runtime.Agents;
using Wolverine.Runtime.Routing;
using Wolverine.Transports;
Expand Down Expand Up @@ -155,7 +156,13 @@ public IMessageRouter RoutingFor(Type messageType)
? typeof(MessageRouter<>).CloseAndBuildAs<IMessageRouter>(this, routes, messageType)
: typeof(EmptyMessageRouter<>).CloseAndBuildAs<IMessageRouter>(this, messageType);

Observer.MessageRouted(messageType, router);
// Skip framework-internal types (IAgentCommand, INotToBeRouted, IInternalMessage,
// and types from assemblies marked [ExcludeFromServiceCapabilities]) so they
// never reach observers like CritterWatch. See GH-2520.
if (!messageType.IsSystemMessageType())
{
Observer.MessageRouted(messageType, router);
}

_messageTypeRouting = _messageTypeRouting.AddOrUpdate(messageType, router);

Expand Down
Loading