157 lines
5.5 KiB
C#
157 lines
5.5 KiB
C#
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using StackExchange.Redis;
|
|
using StellaOps.Scanner.WebService.Contracts;
|
|
using StellaOps.Scanner.WebService.Options;
|
|
using StellaOps.Scanner.WebService.Serialization;
|
|
using System;
|
|
using System.Globalization;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace StellaOps.Scanner.WebService.Services;
|
|
|
|
internal sealed class RedisPlatformEventPublisher : IPlatformEventPublisher, IAsyncDisposable
|
|
{
|
|
private readonly ScannerWebServiceOptions.EventsOptions _options;
|
|
private readonly ILogger<RedisPlatformEventPublisher> _logger;
|
|
private readonly IRedisConnectionFactory _connectionFactory;
|
|
private readonly TimeSpan _publishTimeout;
|
|
private readonly string _streamKey;
|
|
private readonly long? _maxStreamLength;
|
|
|
|
private readonly SemaphoreSlim _connectionGate = new(1, 1);
|
|
private IConnectionMultiplexer? _connection;
|
|
private bool _disposed;
|
|
|
|
public RedisPlatformEventPublisher(
|
|
IOptions<ScannerWebServiceOptions> options,
|
|
IRedisConnectionFactory connectionFactory,
|
|
ILogger<RedisPlatformEventPublisher> logger)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
ArgumentNullException.ThrowIfNull(connectionFactory);
|
|
|
|
_options = options.Value.Events ?? throw new InvalidOperationException("Events options are required when redis publisher is registered.");
|
|
if (!_options.Enabled)
|
|
{
|
|
throw new InvalidOperationException("RedisPlatformEventPublisher requires events emission to be enabled.");
|
|
}
|
|
|
|
if (!string.Equals(_options.Driver, "redis", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new InvalidOperationException($"RedisPlatformEventPublisher cannot be used with driver '{_options.Driver}'.");
|
|
}
|
|
|
|
_connectionFactory = connectionFactory;
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
_streamKey = string.IsNullOrWhiteSpace(_options.Stream) ? "stella.events" : _options.Stream;
|
|
_publishTimeout = TimeSpan.FromSeconds(_options.PublishTimeoutSeconds <= 0 ? 5 : _options.PublishTimeoutSeconds);
|
|
_maxStreamLength = _options.MaxStreamLength > 0 ? _options.MaxStreamLength : null;
|
|
}
|
|
|
|
public async Task PublishAsync(JobEngineEvent @event, CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(@event);
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
var database = await GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
|
|
var payload = JobEngineEventSerializer.Serialize(@event);
|
|
|
|
var entries = new NameValueEntry[]
|
|
{
|
|
new("event", payload),
|
|
new("kind", @event.Kind),
|
|
new("tenant", @event.Tenant),
|
|
new("occurredAt", @event.OccurredAt.ToString("O", CultureInfo.InvariantCulture)),
|
|
new("idempotencyKey", @event.IdempotencyKey)
|
|
};
|
|
|
|
int? maxLength = null;
|
|
if (_maxStreamLength.HasValue)
|
|
{
|
|
var clamped = Math.Min(_maxStreamLength.Value, int.MaxValue);
|
|
maxLength = (int)clamped;
|
|
}
|
|
|
|
var publishTask = maxLength.HasValue
|
|
? database.StreamAddAsync(_streamKey, entries, maxLength: maxLength, useApproximateMaxLength: true)
|
|
: database.StreamAddAsync(_streamKey, entries);
|
|
|
|
if (_publishTimeout > TimeSpan.Zero)
|
|
{
|
|
await publishTask.WaitAsync(_publishTimeout, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
else
|
|
{
|
|
await publishTask.ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private async Task<IDatabase> GetDatabaseAsync(CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
if (_connection is not null && _connection.IsConnected)
|
|
{
|
|
return _connection.GetDatabase();
|
|
}
|
|
|
|
await _connectionGate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
if (_connection is null || !_connection.IsConnected)
|
|
{
|
|
var config = ConfigurationOptions.Parse(_options.Dsn);
|
|
config.AbortOnConnectFail = false;
|
|
|
|
if (_options.DriverSettings.TryGetValue("clientName", out var clientName) && !string.IsNullOrWhiteSpace(clientName))
|
|
{
|
|
config.ClientName = clientName;
|
|
}
|
|
|
|
if (_options.DriverSettings.TryGetValue("ssl", out var sslValue) && bool.TryParse(sslValue, out var ssl))
|
|
{
|
|
config.Ssl = ssl;
|
|
}
|
|
|
|
_connection = await _connectionFactory.ConnectAsync(config, cancellationToken).ConfigureAwait(false);
|
|
_logger.LogInformation("Connected Redis platform event publisher to stream {Stream}.", _streamKey);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_connectionGate.Release();
|
|
}
|
|
|
|
return _connection!.GetDatabase();
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_disposed = true;
|
|
|
|
if (_connection is not null)
|
|
{
|
|
try
|
|
{
|
|
await _connection.CloseAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogDebug(ex, "Error while closing Redis platform event publisher connection.");
|
|
}
|
|
|
|
_connection.Dispose();
|
|
}
|
|
|
|
_connectionGate.Dispose();
|
|
}
|
|
}
|