using System.Text.Json;
using System.Text.Json.Serialization;
namespace IronTelemetry.Client;
///
/// File-based offline queue for telemetry items.
/// Persists failed sends to disk and retries with exponential backoff.
///
public class OfflineQueue : IDisposable
{
private readonly string _queueFilePath;
private readonly int _maxQueueSize;
private readonly object _fileLock = new();
private readonly SemaphoreSlim _retrySemaphore = new(1, 1);
private readonly Timer? _retryTimer;
private readonly Func, Task> _sendFunc;
private int _retryAttempt;
private bool _disposed;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false
};
///
/// Creates a new offline queue.
///
/// Function to send items to server. Returns true on success.
/// Directory to store queue file. Defaults to app data.
/// Maximum items to store. Oldest items dropped when exceeded.
/// Whether to automatically retry sending queued items.
public OfflineQueue(
Func, Task> sendFunc,
string? queueDirectory = null,
int maxQueueSize = 1000,
bool enableAutoRetry = true)
{
_sendFunc = sendFunc;
_maxQueueSize = maxQueueSize;
var directory = queueDirectory ?? GetDefaultQueueDirectory();
Directory.CreateDirectory(directory);
_queueFilePath = Path.Combine(directory, "telemetry_queue.json");
if (enableAutoRetry)
{
// Start retry timer - initial delay 30 seconds, then every 60 seconds
_retryTimer = new Timer(
_ => _ = RetryQueuedItemsAsync(),
null,
TimeSpan.FromSeconds(30),
TimeSpan.FromSeconds(60));
}
}
///
/// Number of items currently in the queue.
///
public int Count
{
get
{
lock (_fileLock)
{
var items = LoadQueue();
return items.Count;
}
}
}
///
/// Enqueue items that failed to send.
///
public void Enqueue(List items)
{
if (items.Count == 0) return;
lock (_fileLock)
{
var queue = LoadQueue();
queue.AddRange(items);
// Trim to max size (remove oldest)
while (queue.Count > _maxQueueSize)
{
queue.RemoveAt(0);
}
SaveQueue(queue);
}
}
///
/// Try to send all queued items.
///
public async Task RetryQueuedItemsAsync()
{
if (!await _retrySemaphore.WaitAsync(0))
{
// Already retrying
return false;
}
try
{
List items;
lock (_fileLock)
{
items = LoadQueue();
}
if (items.Count == 0)
{
_retryAttempt = 0;
return true;
}
// Try to send
var success = await _sendFunc(items);
if (success)
{
// Clear the queue
lock (_fileLock)
{
SaveQueue(new List());
}
_retryAttempt = 0;
return true;
}
else
{
// Exponential backoff - adjust retry timer
_retryAttempt++;
return false;
}
}
catch
{
_retryAttempt++;
return false;
}
finally
{
_retrySemaphore.Release();
}
}
///
/// Clear all queued items without sending.
///
public void Clear()
{
lock (_fileLock)
{
SaveQueue(new List());
}
}
///
/// Get all queued items (for display/export).
///
public List GetQueuedItems()
{
lock (_fileLock)
{
return LoadQueue();
}
}
private List LoadQueue()
{
try
{
if (!File.Exists(_queueFilePath))
{
return new List();
}
var json = File.ReadAllText(_queueFilePath);
return JsonSerializer.Deserialize>(json, JsonOptions)
?? new List();
}
catch
{
return new List();
}
}
private void SaveQueue(List items)
{
try
{
var json = JsonSerializer.Serialize(items, JsonOptions);
File.WriteAllText(_queueFilePath, json);
}
catch
{
// Ignore write errors
}
}
private static string GetDefaultQueueDirectory()
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"IronTelemetry",
"Queue");
}
public void Dispose()
{
if (!_disposed)
{
_retryTimer?.Dispose();
_retrySemaphore.Dispose();
_disposed = true;
}
}
}
///
/// Options for offline queue behavior.
///
public class OfflineQueueOptions
{
///
/// Directory to store queue files. Defaults to LocalApplicationData/IronTelemetry/Queue.
///
public string? QueueDirectory { get; set; }
///
/// Maximum number of items to store in the queue. Oldest items dropped when exceeded.
/// Default: 1000
///
public int MaxQueueSize { get; set; } = 1000;
///
/// Whether to automatically retry sending queued items in the background.
/// Default: true
///
public bool EnableAutoRetry { get; set; } = true;
///
/// Initial retry delay after a failed send. Default: 30 seconds.
///
public TimeSpan InitialRetryDelay { get; set; } = TimeSpan.FromSeconds(30);
///
/// Maximum retry delay (for exponential backoff). Default: 5 minutes.
///
public TimeSpan MaxRetryDelay { get; set; } = TimeSpan.FromMinutes(5);
}