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); }