Add resilient upload queue and client health
This commit is contained in:
@@ -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)
|
||||
|
||||
106
src/OCSentinelCli/Transport/UploadQueue.cs
Normal file
106
src/OCSentinelCli/Transport/UploadQueue.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user