From d1f90f1fb6aef9e62d55984554f056fb081216da Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 9 Jan 2026 14:39:26 -0800 Subject: [PATCH] fix: Retry after partial file reads. --- .../Internal/DataSources/FileDataSource.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pkgs/sdk/server/src/Internal/DataSources/FileDataSource.cs b/pkgs/sdk/server/src/Internal/DataSources/FileDataSource.cs index 91cb989b..98ea34c8 100644 --- a/pkgs/sdk/server/src/Internal/DataSources/FileDataSource.cs +++ b/pkgs/sdk/server/src/Internal/DataSources/FileDataSource.cs @@ -28,6 +28,10 @@ internal sealed class FileDataSource : IDataSource private volatile int _lastVersion; private object _updateLock = new object(); + private const int MaxRetries = 5; + private readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(0.6); + private readonly Dictionary _retryCounts = new Dictionary(); + public FileDataSource(IDataSourceUpdates dataSourceUpdates, FileDataTypes.IFileReader fileReader, List paths, bool autoUpdate, Func alternateParser, bool skipMissingPaths, FileDataTypes.DuplicateKeysHandling duplicateKeysHandling, @@ -102,17 +106,51 @@ private void LoadAll() _logger.Debug("file data: {0}", content); var data = _parser.Parse(content, version); _dataMerger.AddToData(data, flags, segments); + // Remove any retry count associated with this path. + _retryCounts.Remove(path); } catch (FileNotFoundException) when (_skipMissingPaths) { _logger.Debug("{0}: {1}", path, "File not found"); } + catch (System.Text.Json.JsonException) + { + // We may have received the notification of a file change while the file was being written. + // So we may read an empty or partially written file. So, when we encounter a JSON parsing issue + // we will retry after a short delay. + // We will retry up to MaxRetries times before giving up. + if (!_retryCounts.ContainsKey(path)) + { + _retryCounts[path] = 0; + } + _retryCounts[path]++; + + if (_retryCounts[path] < MaxRetries) + { + _logger.Warn("{0}: {1}", path, "Failed to parse file, retrying in " + RetryDelay.TotalMilliseconds + " milliseconds"); + Task.Run(async () => + { + await Task.Delay(RetryDelay); + LoadAll(); + }); + } + else + { + _logger.Error("{0}: {1}", path, "Failed to parse file after " + MaxRetries + " retries"); + } + + return; + } catch (Exception e) { LogHelpers.LogException(_logger, "Failed to load " + path, e); return; } } + + // If any files failed to load, from anything other than not existing, then that + // update would fail. This behavior is retained with the addition of the retry. But it should be + // examined. var allData = new FullDataSet( ImmutableDictionary.Create>()