feat: add advisory event publication for API operations (Gap 7.6)

Add JetStream advisory subject constants to EventSubjects and a lightweight
AdvisoryPublisher that publishes stream/consumer lifecycle events to
$JS.EVENT.ADVISORY.* subjects without depending on InternalEventSystem
directly (testable via Action delegate injection).
This commit is contained in:
Joseph Doherty
2026-02-25 10:56:11 -05:00
parent c0d206102d
commit 2c52b69c93
3 changed files with 350 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
namespace NATS.Server.JetStream.Api;
/// <summary>
/// Publishes JetStream advisory events to $JS.EVENT.ADVISORY.* subjects.
/// Designed to be lightweight and testable; accepts a publish action delegate
/// rather than depending directly on InternalEventSystem.
/// Go reference: jetstream_api.go advisory publication.
/// </summary>
public sealed class AdvisoryPublisher
{
private readonly Action<string, object> _publishAction;
private long _publishCount;
public AdvisoryPublisher(Action<string, object> publishAction)
{
_publishAction = publishAction;
}
/// <summary>
/// Total number of advisory events published since creation.
/// </summary>
public long PublishCount => Interlocked.Read(ref _publishCount);
/// <summary>
/// Publishes a stream created advisory.
/// Go reference: jetstream_api.go — advisory on stream creation.
/// </summary>
public void StreamCreated(string streamName, object? detail = null)
{
var subject = string.Format(Events.EventSubjects.JsAdvisoryStreamCreated, streamName);
Publish(subject, new AdvisoryEvent
{
Type = "io.nats.jetstream.advisory.stream_created",
Stream = streamName,
TimeStamp = DateTime.UtcNow,
Detail = detail,
});
}
/// <summary>
/// Publishes a stream deleted advisory.
/// Go reference: jetstream_api.go — advisory on stream deletion.
/// </summary>
public void StreamDeleted(string streamName)
{
var subject = string.Format(Events.EventSubjects.JsAdvisoryStreamDeleted, streamName);
Publish(subject, new AdvisoryEvent
{
Type = "io.nats.jetstream.advisory.stream_deleted",
Stream = streamName,
TimeStamp = DateTime.UtcNow,
});
}
/// <summary>
/// Publishes a stream updated advisory.
/// Go reference: jetstream_api.go — advisory on stream config update.
/// </summary>
public void StreamUpdated(string streamName, object? detail = null)
{
var subject = string.Format(Events.EventSubjects.JsAdvisoryStreamUpdated, streamName);
Publish(subject, new AdvisoryEvent
{
Type = "io.nats.jetstream.advisory.stream_updated",
Stream = streamName,
TimeStamp = DateTime.UtcNow,
Detail = detail,
});
}
/// <summary>
/// Publishes a consumer created advisory.
/// Go reference: jetstream_api.go — advisory on consumer creation.
/// </summary>
public void ConsumerCreated(string streamName, string consumerName)
{
var subject = string.Format(Events.EventSubjects.JsAdvisoryConsumerCreated, streamName, consumerName);
Publish(subject, new AdvisoryEvent
{
Type = "io.nats.jetstream.advisory.consumer_created",
Stream = streamName,
Consumer = consumerName,
TimeStamp = DateTime.UtcNow,
});
}
/// <summary>
/// Publishes a consumer deleted advisory.
/// Go reference: jetstream_api.go — advisory on consumer deletion.
/// </summary>
public void ConsumerDeleted(string streamName, string consumerName)
{
var subject = string.Format(Events.EventSubjects.JsAdvisoryConsumerDeleted, streamName, consumerName);
Publish(subject, new AdvisoryEvent
{
Type = "io.nats.jetstream.advisory.consumer_deleted",
Stream = streamName,
Consumer = consumerName,
TimeStamp = DateTime.UtcNow,
});
}
private void Publish(string subject, AdvisoryEvent evt)
{
Interlocked.Increment(ref _publishCount);
_publishAction(subject, evt);
}
}
/// <summary>
/// Advisory event payload describing a JetStream lifecycle event.
/// Go reference: jetstream_api.go advisory event types.
/// </summary>
public sealed class AdvisoryEvent
{
/// <summary>Reverse-DNS event type identifier (e.g., "io.nats.jetstream.advisory.stream_created").</summary>
public string Type { get; init; } = string.Empty;
/// <summary>Name of the stream involved in the event, if applicable.</summary>
public string? Stream { get; init; }
/// <summary>Name of the consumer involved in the event, if applicable.</summary>
public string? Consumer { get; init; }
/// <summary>UTC timestamp when the advisory was generated.</summary>
public DateTime TimeStamp { get; init; }
/// <summary>Optional additional detail payload (arbitrary object).</summary>
public object? Detail { get; init; }
}