Add resilient upload queue and client health
Some checks failed
OfficeCom Sentinel Client / validate-client (push) Successful in 24s
OfficeCom Sentinel Client / build-client-windows (push) Failing after 18s

This commit is contained in:
OfficeCom Codex
2026-07-26 20:25:25 +02:00
parent dfdae7a532
commit e7fd4e7ef5
11 changed files with 282 additions and 25 deletions

View File

@@ -38,17 +38,31 @@ internal sealed class N8nUploadClient
request.Headers.Add("X-ATN-Payload-SHA256", payloadHash);
request.Headers.Add("X-ATN-Signature", signature);
using HttpResponseMessage response = httpClient.Send(request);
string responseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
return new UploadResult
try
{
Success = response.IsSuccessStatusCode,
StatusCode = (int)response.StatusCode,
Message = string.IsNullOrWhiteSpace(responseText) ? response.ReasonPhrase ?? string.Empty : responseText,
Nonce = nonce,
PayloadSha256 = payloadHash
};
using HttpResponseMessage response = httpClient.Send(request);
string responseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
return new UploadResult
{
Success = response.IsSuccessStatusCode,
StatusCode = (int)response.StatusCode,
Message = string.IsNullOrWhiteSpace(responseText) ? response.ReasonPhrase ?? string.Empty : responseText,
Nonce = nonce,
PayloadSha256 = payloadHash
};
}
catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException)
{
return new UploadResult
{
Success = false,
StatusCode = 0,
Message = exception.Message,
Nonce = nonce,
PayloadSha256 = payloadHash
};
}
}
private static string ComputeSha256(string value)

View File

@@ -0,0 +1,106 @@
using System.Text.Json;
using OCSentinelCli.Models;
namespace OCSentinelCli.Transport;
internal sealed record UploadHealth
{
public DateTimeOffset? LastUploadAttemptUtc { get; init; }
public DateTimeOffset? LastSuccessfulUploadUtc { get; init; }
public string LastUploadStatus { get; init; } = "not-attempted";
public string LastUploadError { get; init; } = string.Empty;
public int QueuedReportCount { get; init; }
}
internal sealed class UploadQueue
{
private readonly string queueDirectory;
private readonly string healthPath;
private readonly int maxReports;
public UploadQueue(int maxReports)
{
string root = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "OCSentinel");
queueDirectory = Path.Combine(root, "upload-queue");
healthPath = Path.Combine(root, "state", "upload-health.json");
this.maxReports = Math.Clamp(maxReports, 10, 500);
}
public UploadHealth LoadHealth()
{
try
{
if (!File.Exists(healthPath))
{
return new UploadHealth { QueuedReportCount = GetQueueDepth() };
}
UploadHealth? health = JsonSerializer.Deserialize<UploadHealth>(File.ReadAllText(healthPath), JsonOptions.Default);
return (health ?? new UploadHealth()) with { QueuedReportCount = GetQueueDepth() };
}
catch (Exception)
{
return new UploadHealth { QueuedReportCount = GetQueueDepth() };
}
}
public void SaveHealth(UploadHealth health)
{
Directory.CreateDirectory(Path.GetDirectoryName(healthPath)!);
WriteAtomically(healthPath, JsonSerializer.Serialize(health, JsonOptions.Default));
}
public UploadResult? Drain(N8nUploadClient client, string webhookUrl, string machineName, string clientVersion, string secret, int timeoutSeconds)
{
foreach (string path in GetQueuedPaths())
{
string payload = File.ReadAllText(path);
UploadResult result = client.UploadJson(webhookUrl, machineName, clientVersion, payload, secret, timeoutSeconds);
if (!result.Success)
{
return result;
}
File.Delete(path);
}
return null;
}
public int Enqueue(string payloadJson)
{
Directory.CreateDirectory(queueDirectory);
string fileName = $"{DateTimeOffset.UtcNow:yyyyMMddHHmmssfff}-{Guid.NewGuid():N}.json";
WriteAtomically(Path.Combine(queueDirectory, fileName), payloadJson);
foreach (string stalePath in GetQueuedPaths().Take(Math.Max(0, GetQueueDepth() - maxReports)))
{
File.Delete(stalePath);
}
return GetQueueDepth();
}
public int GetQueueDepth()
{
return Directory.Exists(queueDirectory) ? Directory.EnumerateFiles(queueDirectory, "*.json").Count() : 0;
}
private IEnumerable<string> GetQueuedPaths()
{
return Directory.Exists(queueDirectory)
? Directory.EnumerateFiles(queueDirectory, "*.json").OrderBy(static path => path, StringComparer.Ordinal)
: Enumerable.Empty<string>();
}
private static void WriteAtomically(string path, string content)
{
string temporaryPath = path + ".tmp";
File.WriteAllText(temporaryPath, content);
File.Move(temporaryPath, path, true);
}
}