Phase 8: Production readiness — failover tests, security hardening, sandboxing, deployment docs
- WP-1-3: Central/site failover + dual-node recovery tests (17 tests) - WP-4: Performance testing framework for target scale (7 tests) - WP-5: Security hardening (LDAPS, JWT key length, no secrets in logs) (11 tests) - WP-6: Script sandboxing adversarial tests (28 tests, all forbidden APIs) - WP-7: Recovery drill test scaffolds (5 tests) - WP-8: Observability validation (structured logs, correlation IDs, metrics) (6 tests) - WP-9: Message contract compatibility (forward/backward compat) (18 tests) - WP-10: Deployment packaging (installation guide, production checklist, topology) - WP-11: Operational runbooks (failover, troubleshooting, maintenance) 92 new tests, all passing. Zero warnings.
This commit is contained in:
98
src/ScadaLink.ExternalSystemGateway/DatabaseGateway.cs
Normal file
98
src/ScadaLink.ExternalSystemGateway/DatabaseGateway.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System.Data.Common;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ScadaLink.Commons.Entities.ExternalSystems;
|
||||
using ScadaLink.Commons.Interfaces.Repositories;
|
||||
using ScadaLink.Commons.Interfaces.Services;
|
||||
using ScadaLink.Commons.Types.Enums;
|
||||
using ScadaLink.StoreAndForward;
|
||||
|
||||
namespace ScadaLink.ExternalSystemGateway;
|
||||
|
||||
/// <summary>
|
||||
/// WP-9: Database access from scripts.
|
||||
/// Database.Connection("name") — returns ADO.NET SqlConnection (connection pooling).
|
||||
/// Database.CachedWrite("name", "sql", params) — submits to S&F engine.
|
||||
/// </summary>
|
||||
public class DatabaseGateway : IDatabaseGateway
|
||||
{
|
||||
private readonly IExternalSystemRepository _repository;
|
||||
private readonly StoreAndForwardService? _storeAndForward;
|
||||
private readonly ILogger<DatabaseGateway> _logger;
|
||||
|
||||
public DatabaseGateway(
|
||||
IExternalSystemRepository repository,
|
||||
ILogger<DatabaseGateway> logger,
|
||||
StoreAndForwardService? storeAndForward = null)
|
||||
{
|
||||
_repository = repository;
|
||||
_logger = logger;
|
||||
_storeAndForward = storeAndForward;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an open SqlConnection from the named database connection definition.
|
||||
/// Connection pooling is managed by the underlying ADO.NET provider.
|
||||
/// </summary>
|
||||
public async Task<DbConnection> GetConnectionAsync(
|
||||
string connectionName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var definition = await ResolveConnectionAsync(connectionName, cancellationToken);
|
||||
if (definition == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Database connection '{connectionName}' not found");
|
||||
}
|
||||
|
||||
var connection = new SqlConnection(definition.ConnectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
return connection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submits a SQL write to the store-and-forward engine for reliable delivery.
|
||||
/// </summary>
|
||||
public async Task CachedWriteAsync(
|
||||
string connectionName,
|
||||
string sql,
|
||||
IReadOnlyDictionary<string, object?>? parameters = null,
|
||||
string? originInstanceName = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var definition = await ResolveConnectionAsync(connectionName, cancellationToken);
|
||||
if (definition == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Database connection '{connectionName}' not found");
|
||||
}
|
||||
|
||||
if (_storeAndForward == null)
|
||||
{
|
||||
throw new InvalidOperationException("Store-and-forward service not available for cached writes");
|
||||
}
|
||||
|
||||
var payload = JsonSerializer.Serialize(new
|
||||
{
|
||||
ConnectionName = connectionName,
|
||||
Sql = sql,
|
||||
Parameters = parameters
|
||||
});
|
||||
|
||||
await _storeAndForward.EnqueueAsync(
|
||||
StoreAndForwardCategory.CachedDbWrite,
|
||||
connectionName,
|
||||
payload,
|
||||
originInstanceName,
|
||||
definition.MaxRetries > 0 ? definition.MaxRetries : null,
|
||||
definition.RetryDelay > TimeSpan.Zero ? definition.RetryDelay : null);
|
||||
}
|
||||
|
||||
private async Task<DatabaseConnectionDefinition?> ResolveConnectionAsync(
|
||||
string connectionName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var connections = await _repository.GetAllDatabaseConnectionsAsync(cancellationToken);
|
||||
return connections.FirstOrDefault(c =>
|
||||
c.Name.Equals(connectionName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user