using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Messaging;
using StellaOps.Messaging.Abstractions;
using StellaOps.Scheduler.WebService.Options;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Scheduler.WebService.GraphJobs.Events;
///
/// Transport-agnostic implementation of using StellaOps.Messaging abstractions.
/// Works with any configured transport (Valkey, PostgreSQL, InMemory).
///
internal sealed class MessagingGraphJobEventPublisher : IGraphJobCompletionPublisher
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private readonly IOptionsMonitor _options;
private readonly IEventStream _eventStream;
private readonly ILogger _logger;
public MessagingGraphJobEventPublisher(
IOptionsMonitor options,
IEventStreamFactory eventStreamFactory,
ILogger logger)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(eventStreamFactory);
_options = options;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
var eventsOptions = options.CurrentValue?.GraphJobs ?? new GraphJobEventsOptions();
var streamKey = string.IsNullOrWhiteSpace(eventsOptions.Stream) ? "stella.events" : eventsOptions.Stream;
var maxStreamLength = eventsOptions.MaxStreamLength > 0 ? eventsOptions.MaxStreamLength : (long?)null;
_eventStream = eventStreamFactory.Create(new EventStreamOptions
{
StreamName = streamKey,
MaxLength = maxStreamLength,
ApproximateTrimming = true,
});
_logger.LogInformation("Initialized messaging graph job event publisher for stream {Stream}.", streamKey);
}
public async Task PublishAsync(GraphJobCompletionNotification notification, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(notification);
var options = _options.CurrentValue?.GraphJobs ?? new GraphJobEventsOptions();
if (!options.Enabled)
{
_logger.LogDebug("Graph job events disabled; skipping emission for {JobId}.", notification.Job.Id);
return;
}
try
{
var envelope = GraphJobEventFactory.Create(notification);
var publishOptions = new EventPublishOptions
{
TenantId = envelope.Tenant,
MaxStreamLength = options.MaxStreamLength > 0 ? options.MaxStreamLength : null,
Headers = new Dictionary
{
["kind"] = envelope.Kind,
["occurredAt"] = envelope.Timestamp.ToString("O", CultureInfo.InvariantCulture),
["jobId"] = notification.Job.Id,
["status"] = notification.Status.ToString()
}
};
var publishTask = _eventStream.PublishAsync(envelope, publishOptions, cancellationToken);
if (options.PublishTimeoutSeconds > 0)
{
var timeout = TimeSpan.FromSeconds(options.PublishTimeoutSeconds);
await publishTask.AsTask().WaitAsync(timeout, cancellationToken).ConfigureAwait(false);
}
else
{
await publishTask.ConfigureAwait(false);
}
_logger.LogDebug("Published graph job event {JobId} to stream.", notification.Job.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to publish graph job completion for {JobId}; logging payload instead.", notification.Job.Id);
LogEnvelope(notification);
}
}
private void LogEnvelope(GraphJobCompletionNotification notification)
{
var envelope = GraphJobEventFactory.Create(notification);
var json = JsonSerializer.Serialize(envelope, SerializerOptions);
_logger.LogInformation("{EventJson}", json);
}
}