Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
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;
|
||||
|
||||
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(OrchestratorEvent @event, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(@event);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var database = await GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
|
||||
var payload = OrchestratorEventSerializer.Serialize(@event);
|
||||
|
||||
var entries = new NameValueEntry[]
|
||||
{
|
||||
new("event", payload),
|
||||
new("kind", @event.Kind),
|
||||
new("tenant", @event.Tenant),
|
||||
new("occurredAt", @event.OccurredAt.ToString("O")),
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user