Update EonaCat.Connections/Processors/JsonDataProcessor.cs

This commit is contained in:
2026-01-27 06:15:22 +00:00
parent b38514f0d1
commit 94119081bb

View File

@@ -1,38 +1,23 @@
using EonaCat.Json; using EonaCat.Json;
using Heijmans.Connector.Models;
using System;
using System.Text; using System.Text;
namespace EonaCat.Connections.Processors namespace EonaCat.Connections.Processors
{ {
public class ProcessedJsonMessage<TData>
{
public TData Data { get; set; }
public string RawData { get; set; } = string.Empty;
public string ClientName { get; set; } = string.Empty;
public string ClientEndpoint { get; set; } = string.Empty;
}
public class ProcessedTextMessage
{
public string Text { get; set; } = string.Empty;
public string ClientName { get; set; } = string.Empty;
}
public sealed class JsonDataProcessor<TData> : IDisposable public sealed class JsonDataProcessor<TData> : IDisposable
{ {
private readonly StringBuilder _buffer = new StringBuilder(); private readonly StringBuilder _buffer = new StringBuilder(4096);
private readonly object _syncLock = new object(); private readonly object _sync = new object();
private bool _isDisposed; private bool _disposed;
// 30 MB default max buffer size
public int MaxAllowedBufferSize { get; set; } = 30 * 1024 * 1024; public int MaxAllowedBufferSize { get; set; } = 30 * 1024 * 1024;
public int MaxMessagesPerBatch { get; set; } = 200; public int MaxMessagesPerBatch { get; set; } = 200;
public string ClientName { get; } public string ClientName { get; }
// Diagnostics
public long TotalBytesProcessed { get; private set; } public long TotalBytesProcessed { get; private set; }
public long TotalChunksReceived { get; private set; } public long TotalChunksReceived { get; private set; }
// Events
public event EventHandler<ProcessedJsonMessage<TData>>? OnProcessMessage; public event EventHandler<ProcessedJsonMessage<TData>>? OnProcessMessage;
public event EventHandler<ProcessedTextMessage>? OnProcessTextMessage; public event EventHandler<ProcessedTextMessage>? OnProcessTextMessage;
public event EventHandler<Exception>? OnMessageError; public event EventHandler<Exception>? OnMessageError;
@@ -42,96 +27,48 @@ namespace EonaCat.Connections.Processors
ClientName = Guid.NewGuid().ToString(); ClientName = Guid.NewGuid().ToString();
} }
public void Process(string jsonString, string? clientName = null, string? endpoint = null) public void Process(string data, string? client = null, string? endpoint = null)
{ {
ThrowIfDisposed(); ThrowIfDisposed();
if (string.IsNullOrEmpty(jsonString)) if (string.IsNullOrEmpty(data))
{ {
return; return;
} }
TotalChunksReceived++; TotalChunksReceived++;
TotalBytesProcessed += Encoding.UTF8.GetByteCount(jsonString); TotalBytesProcessed += Encoding.UTF8.GetByteCount(data);
ProcessInternal(jsonString, clientName, endpoint); ProcessInternal(data, client ?? ClientName, endpoint);
} }
public void Process(DataReceivedEventArgs e, string? clientName = null) private void ProcessInternal(string data, string client, string? endpoint)
{ {
ThrowIfDisposed(); lock (_sync)
if (e == null)
{ {
return; _buffer.Append(data);
}
string dataString; int processed = 0;
if (e.IsBinary) while (processed < MaxMessagesPerBatch &&
TryExtract(out int start, out int length, out bool isText))
{ {
if (e.Data == null || e.Data.Length == 0) if (isText)
{ {
return; var text = _buffer.ToString(start, length);
}
dataString = Encoding.UTF8.GetString(e.Data);
}
else
{
dataString = e.StringData;
}
if (string.IsNullOrWhiteSpace(dataString))
{
return;
}
string client = e.Nickname ?? clientName ?? ClientName;
TotalChunksReceived++;
TotalBytesProcessed += Encoding.UTF8.GetByteCount(dataString);
ProcessInternal(dataString, client, e.RemoteEndPoint?.ToString());
}
private void ProcessInternal(string jsonString, string? clientName, string? endpoint)
{
string client = clientName ?? ClientName;
lock (_syncLock)
{
_buffer.Append(jsonString);
int processedCount = 0;
while (processedCount < MaxMessagesPerBatch &&
TryExtractFullJson(out int fullJsonStart, out int fullJsonLength,
out int textStart, out int textLength))
{
// Check if we have any leading text to process
if (textLength > 0)
{
string text = _buffer.ToString(textStart, textLength);
OnProcessTextMessage?.Invoke(this, new ProcessedTextMessage OnProcessTextMessage?.Invoke(this, new ProcessedTextMessage
{ {
Text = text, Text = text,
ClientName = client ClientName = client
}); });
// Remove the processed text immediately
_buffer.Remove(textStart, textLength);
processedCount++;
continue;
} }
else
// Process the full JSON
if (fullJsonLength > 0)
{ {
string json = _buffer.ToString(fullJsonStart, fullJsonLength); var json = _buffer.ToString(start, length);
try try
{ {
var deserialized = JsonHelper.ToObject<TData>(json); var obj = JsonHelper.ToObject<TData>(json);
OnProcessMessage?.Invoke(this, new ProcessedJsonMessage<TData> OnProcessMessage?.Invoke(this, new ProcessedJsonMessage<TData>
{ {
Data = deserialized, Data = obj,
RawData = json, RawData = json,
ClientName = client, ClientName = client,
ClientEndpoint = endpoint ?? string.Empty ClientEndpoint = endpoint ?? string.Empty
@@ -139,102 +76,87 @@ namespace EonaCat.Connections.Processors
} }
catch (Exception ex) catch (Exception ex)
{ {
OnMessageError?.Invoke(this, new Exception( OnMessageError?.Invoke(this,
$"Failed to deserialize JSON for client {client}", ex)); new Exception($"Failed to parse JSON for {client}", ex));
}
// Remove processed JSON immediately
_buffer.Remove(fullJsonStart, fullJsonLength);
processedCount++;
} }
} }
// Prevent buffer overflow Consume(start, length);
processed++;
}
if (_buffer.Length > MaxAllowedBufferSize) if (_buffer.Length > MaxAllowedBufferSize)
{ {
OnMessageError?.Invoke(this, new Exception( OnMessageError?.Invoke(this,
$"Buffer overflow for client {client}. Current size: {_buffer.Length / 1024.0 / 1024.0:F2} MB. " + new Exception($"Buffer exceeded {MaxAllowedBufferSize} bytes for client {client}. Discarding."));
$"Max allowed: {MaxAllowedBufferSize / 1024.0 / 1024.0:F2} MB. Clearing buffer."));
_buffer.Clear(); _buffer.Clear();
_buffer.Capacity = 4096;
} }
} }
} }
private bool TryExtractFullJson( private bool TryExtract(out int start, out int length, out bool isText)
out int fullJsonStart,
out int fullJsonLength,
out int textStart,
out int textLength)
{ {
fullJsonStart = fullJsonLength = textStart = textLength = 0; start = length = 0;
isText = false;
if (_buffer.Length == 0) if (_buffer.Length == 0)
{ {
return false; return false;
} }
var span = _buffer.ToString().AsSpan();
int pos = 0; int pos = 0;
// Skip leading whitespace while (pos < span.Length && char.IsWhiteSpace(span[pos]))
while (pos < _buffer.Length && char.IsWhiteSpace(_buffer[pos]))
{ {
pos++; pos++;
} }
if (pos >= _buffer.Length) if (pos >= span.Length)
{ {
return false; return false;
} }
// Json needs to start with { or [ char c = span[pos];
if (_buffer[pos] != '{' && _buffer[pos] != '[')
if (c != '{' && c != '[')
{ {
textStart = pos; isText = true;
while (pos < _buffer.Length && _buffer[pos] != '{' && _buffer[pos] != '[') start = pos;
while (pos < span.Length && span[pos] != '{' && span[pos] != '[')
{ {
pos++; pos++;
} }
textLength = pos - textStart; length = pos - start;
return true; return true;
} }
// Check if we also have a json end token start = pos;
fullJsonStart = pos; length = FindJsonEnd(span, pos) - pos;
fullJsonLength = FindJsonTokenEnd(_buffer, pos) - pos; if (length <= 0)
if (fullJsonLength <= 0)
{ {
// partial JSON
return false; return false;
} }
isText = false;
return true; return true;
} }
private static int FindJsonTokenEnd(StringBuilder buffer, int start) private static int FindJsonEnd(ReadOnlySpan<char> span, int start)
{ {
if (start >= buffer.Length) char open = span[start];
{ char close = open == '{' ? '}' : ']';
return 0;
}
int i = start;
char firstChar = buffer[start];
if (firstChar == '{' || firstChar == '[')
{
char open = firstChar;
char close = firstChar == '{' ? '}' : ']';
int depth = 1; int depth = 1;
bool inString = false; bool inString = false;
bool escape = false; bool escape = false;
i++; for (int i = start + 1; i < span.Length; i++)
while (i < buffer.Length)
{ {
char c = buffer[i]; char c = span[i];
if (inString) if (inString)
{ {
@@ -261,46 +183,38 @@ namespace EonaCat.Connections.Processors
{ {
depth++; depth++;
} }
else if (c == close) else if (c == close && --depth == 0)
{
depth--;
if (depth == 0)
{ {
return i + 1; return i + 1;
} }
} }
} }
i++;
}
// partial JSON
return 0; return 0;
} }
// only objects/arrays supported as JSON start private void Consume(int start, int length)
return 0; {
} _buffer.Remove(start, length);
private void ThrowIfDisposed() if (_buffer.Capacity > 1024 * 1024 && _buffer.Length < _buffer.Capacity / 2)
{ {
if (_isDisposed) _buffer.Capacity = Math.Max(_buffer.Length, 4096);
{
throw new ObjectDisposedException(nameof(JsonDataProcessor<TData>));
} }
} }
public void ClearBuffer() public void ClearBuffer()
{ {
lock (_syncLock) lock (_sync)
{ {
_buffer.Clear(); _buffer.Clear();
_buffer.Capacity = 4096;
} }
} }
public void ResetStatistics() public void ResetStatistics()
{ {
lock (_syncLock) lock (_sync)
{ {
TotalBytesProcessed = 0; TotalBytesProcessed = 0;
TotalChunksReceived = 0; TotalChunksReceived = 0;
@@ -309,21 +223,30 @@ namespace EonaCat.Connections.Processors
public void Dispose() public void Dispose()
{ {
if (_isDisposed) if (_disposed)
{ {
return; return;
} }
_isDisposed = true; _disposed = true;
lock (_syncLock) lock (_sync)
{ {
_buffer.Clear(); _buffer.Clear();
_buffer.Capacity = 0;
} }
OnProcessMessage = null; OnProcessMessage = null;
OnProcessTextMessage = null; OnProcessTextMessage = null;
OnMessageError = null; OnMessageError = null;
} }
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(JsonDataProcessor<TData>));
}
}
} }
} }