Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
using System.Text;
|
||||
using Omika.Core.Observability;
|
||||
using Omika.Core.Observability.Outbox;
|
||||
|
||||
namespace Omika.Tests.Observability.Outbox;
|
||||
|
||||
/// <summary>Fabrique de test partagée pour le diagnostic distant (Phase 4).</summary>
|
||||
internal static class OutboxTestHarness
|
||||
{
|
||||
public static TelemetryEvent NewEvent(
|
||||
TelemetryPriority priority = TelemetryPriority.P3,
|
||||
TelemetryLevel level = TelemetryLevel.Info,
|
||||
EventCode? code = null,
|
||||
Guid? eventId = null,
|
||||
TelemetryMetadata? metadata = null) =>
|
||||
new()
|
||||
{
|
||||
EventId = eventId ?? Guid.NewGuid(),
|
||||
EventCode = code ?? TelemetryEventCodes.MobileAppStarted,
|
||||
Level = level,
|
||||
Priority = priority,
|
||||
Metadata = metadata
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Lit un fichier en cohabitation avec un writer encore actif. Les exporteurs
|
||||
/// (LocalFileTelemetryExporter / RemoteDiagnosticIngestionStore) ouvrent en
|
||||
/// <c>FileAccess.Write, FileShare.Read</c> (un writer, lectures autorisées) : un
|
||||
/// lecteur doit donc ouvrir avec <c>FileShare.ReadWrite</c> — jamais
|
||||
/// <see cref="File.ReadAllText"/> / <see cref="File.ReadAllLines"/> dont le
|
||||
/// <c>FileShare.Read</c> rejette l'écriture concurrente.
|
||||
/// </summary>
|
||||
public static string ReadAllTextShared(string path)
|
||||
{
|
||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
|
||||
/// <summary>Équivalent lignes de <see cref="ReadAllTextShared"/> (UTF-8, partage d'écriture).</summary>
|
||||
public static string[] ReadAllLinesShared(string path)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
while (reader.ReadLine() is { } line)
|
||||
{
|
||||
lines.Add(line);
|
||||
}
|
||||
|
||||
return lines.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store en mémoire pour les tests Core (aucune dépendance SQLite). Permet
|
||||
/// d'injecter une panne (ThrowOn) pour vérifier le fail-open de l'outbox.
|
||||
/// </summary>
|
||||
internal sealed class InMemoryDiagnosticOutboxStore : IDiagnosticOutboxStore
|
||||
{
|
||||
private readonly List<DiagnosticOutboxRow> _rows = [];
|
||||
|
||||
public bool ThrowOnStore { get; set; }
|
||||
|
||||
public bool ThrowOnCount { get; set; }
|
||||
|
||||
public Task StoreAsync(IReadOnlyList<DiagnosticOutboxRow> rows, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (ThrowOnStore)
|
||||
{
|
||||
throw new InvalidOperationException("store down");
|
||||
}
|
||||
|
||||
lock (_rows)
|
||||
{
|
||||
foreach (DiagnosticOutboxRow row in rows)
|
||||
{
|
||||
_rows.RemoveAll(r => r.EventId == row.EventId);
|
||||
_rows.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<DiagnosticOutboxRow>> GetNextBatchAsync(
|
||||
int maxCount,
|
||||
long maxBytes,
|
||||
DateTimeOffset now,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_rows)
|
||||
{
|
||||
var result = new List<DiagnosticOutboxRow>();
|
||||
long bytes = 0;
|
||||
foreach (DiagnosticOutboxRow row in _rows
|
||||
.Where(r => r.NextAttemptUtc <= now)
|
||||
.OrderBy(r => r.Priority)
|
||||
.ThenBy(r => r.CreatedAtUtc))
|
||||
{
|
||||
long rowBytes = Encoding.UTF8.GetByteCount(row.PayloadJson);
|
||||
if (result.Count > 0 && bytes + rowBytes > maxBytes)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
result.Add(row);
|
||||
bytes += rowBytes;
|
||||
if (result.Count >= maxCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyList<DiagnosticOutboxRow>>(result);
|
||||
}
|
||||
}
|
||||
|
||||
public Task DeleteAsync(IReadOnlyList<Guid> eventIds, CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_rows)
|
||||
{
|
||||
HashSet<Guid> ids = eventIds.ToHashSet();
|
||||
_rows.RemoveAll(r => ids.Contains(r.EventId));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task UpdateAttemptAsync(
|
||||
IReadOnlyList<Guid> eventIds,
|
||||
int attemptCount,
|
||||
DateTimeOffset nextAttemptUtc,
|
||||
string? lastErrorCode,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_rows)
|
||||
{
|
||||
HashSet<Guid> ids = eventIds.ToHashSet();
|
||||
foreach (DiagnosticOutboxRow row in _rows.Where(r => ids.Contains(r.EventId)).ToList())
|
||||
{
|
||||
int index = _rows.IndexOf(row);
|
||||
_rows[index] = new DiagnosticOutboxRow
|
||||
{
|
||||
EventId = row.EventId,
|
||||
Priority = row.Priority,
|
||||
CreatedAtUtc = row.CreatedAtUtc,
|
||||
ExpiresAtUtc = row.ExpiresAtUtc,
|
||||
PayloadVersion = row.PayloadVersion,
|
||||
PayloadJson = row.PayloadJson,
|
||||
AttemptCount = attemptCount,
|
||||
NextAttemptUtc = nextAttemptUtc,
|
||||
LastAttemptUtc = DateTimeOffset.UtcNow,
|
||||
LastErrorCode = lastErrorCode
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<int> CountAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (ThrowOnCount)
|
||||
{
|
||||
throw new InvalidOperationException("count down");
|
||||
}
|
||||
|
||||
lock (_rows)
|
||||
{
|
||||
return Task.FromResult(_rows.Count);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<int> DeleteExpiredAsync(DateTimeOffset now, CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_rows)
|
||||
{
|
||||
return Task.FromResult(_rows.RemoveAll(r => r.ExpiresAtUtc <= now));
|
||||
}
|
||||
}
|
||||
|
||||
public Task<int> DeleteOldestAsync(TelemetryPriority priority, int count, CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_rows)
|
||||
{
|
||||
List<Guid> toDelete = _rows
|
||||
.Where(r => r.Priority == priority)
|
||||
.OrderBy(r => r.CreatedAtUtc)
|
||||
.Take(count)
|
||||
.Select(r => r.EventId)
|
||||
.ToList();
|
||||
|
||||
_rows.RemoveAll(r => toDelete.Contains(r.EventId));
|
||||
return Task.FromResult(toDelete.Count);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<long> GetDatabaseSizeBytesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_rows)
|
||||
{
|
||||
return Task.FromResult((long)_rows.Count * 100);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<DiagnosticOutboxRow> Snapshot()
|
||||
{
|
||||
lock (_rows)
|
||||
{
|
||||
return _rows.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user