From 33dc6c54f6ee934a177dfbf01540687ab5de19ff Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:58:43 -0800 Subject: [PATCH 1/3] chore: Add per-test hang timeout. --- .github/actions/ci/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index aa34d1d1..3e314ecb 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -75,7 +75,7 @@ runs: shell: bash run: | dotnet restore ${{ inputs.test_project_file }} - dotnet test -v=${{ inputs.test_verbosity }} --framework=${{ inputs.target_test_framework }} ${{ inputs.test_project_file }} + dotnet test --blame-hang-timeout=60s -v=${{ inputs.test_verbosity }} --framework=${{ inputs.target_test_framework }} ${{ inputs.test_project_file }} - name: Remove global.json shell: bash From e059c403e7b7fd8f7825aaedc5cca7d7dd13281e Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:09:01 -0800 Subject: [PATCH 2/3] fix: Coalesce file system watcher reloads. --- .../Internal/DataSources/FileDataSource.cs | 75 ++++++++++++------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/pkgs/sdk/server/src/Internal/DataSources/FileDataSource.cs b/pkgs/sdk/server/src/Internal/DataSources/FileDataSource.cs index 04cc81df..0b97932b 100644 --- a/pkgs/sdk/server/src/Internal/DataSources/FileDataSource.cs +++ b/pkgs/sdk/server/src/Internal/DataSources/FileDataSource.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; @@ -23,6 +23,7 @@ internal sealed class FileDataSource : IDataSource private readonly FileDataTypes.IFileReader _fileReader; private readonly bool _skipMissingPaths; private readonly Logger _logger; + private readonly object _loadLock = new object(); private volatile bool _started; private volatile bool _loadedValidData; private volatile int _lastVersion; @@ -88,35 +89,38 @@ private void Dispose(bool disposing) private void LoadAll() { - var version = Interlocked.Increment(ref _lastVersion); - var flags = new Dictionary(); - var segments = new Dictionary(); - foreach (var path in _paths) + lock (_loadLock) { - try - { - var content = _fileReader.ReadAllText(path); - _logger.Debug("file data: {0}", content); - var data = _parser.Parse(content, version); - _dataMerger.AddToData(data, flags, segments); - } - catch (FileNotFoundException) when (_skipMissingPaths) + var version = Interlocked.Increment(ref _lastVersion); + var flags = new Dictionary(); + var segments = new Dictionary(); + foreach (var path in _paths) { - _logger.Debug("{0}: {1}", path, "File not found"); - } - catch (Exception e) - { - LogHelpers.LogException(_logger, "Failed to load " + path, e); - return; + try + { + var content = _fileReader.ReadAllText(path); + _logger.Debug("file data: {0}", content); + var data = _parser.Parse(content, version); + _dataMerger.AddToData(data, flags, segments); + } + catch (FileNotFoundException) when (_skipMissingPaths) + { + _logger.Debug("{0}: {1}", path, "File not found"); + } + catch (Exception e) + { + LogHelpers.LogException(_logger, "Failed to load " + path, e); + return; + } } + var allData = new FullDataSet( + ImmutableDictionary.Create>() + .SetItem(DataModel.Features, new KeyedItems(flags)) + .SetItem(DataModel.Segments, new KeyedItems(segments)) + ); + _dataSourceUpdates.Init(allData); + _loadedValidData = true; } - var allData = new FullDataSet( - ImmutableDictionary.Create>() - .SetItem(DataModel.Features, new KeyedItems(flags)) - .SetItem(DataModel.Segments, new KeyedItems(segments)) - ); - _dataSourceUpdates.Init(allData); - _loadedValidData = true; } private void TriggerReload() @@ -183,10 +187,15 @@ internal sealed class FileWatchingReloader : IDisposable private readonly ISet _filePaths; private readonly Action _reload; private readonly List _watchers; + private readonly Timer _debounceTimer; + private readonly int _debounceMillis; + private readonly object _timerLock = new object(); - public FileWatchingReloader(List paths, Action reload) + public FileWatchingReloader(List paths, Action reload, int debounceMillis = 100) { _reload = reload; + _debounceMillis = debounceMillis; + _debounceTimer = new Timer(OnDebounceTimerElapsed, null, Timeout.Infinite, Timeout.Infinite); _filePaths = new HashSet(); var dirPaths = new HashSet(); @@ -216,10 +225,19 @@ private void ChangedPath(string path) { if (_filePaths.Contains(path)) { - _reload(); + lock (_timerLock) + { + // Reset the timer to debounce multiple rapid file changes + _debounceTimer.Change(_debounceMillis, Timeout.Infinite); + } } } + private void OnDebounceTimerElapsed(object state) + { + _reload(); + } + public void Dispose() { Dispose(true); @@ -229,6 +247,7 @@ private void Dispose(bool disposing) { if (disposing) { + _debounceTimer?.Dispose(); foreach (var w in _watchers) { w.Dispose(); From 938976233b01fb68c7a949930e26577b927bb84c Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:11:10 -0800 Subject: [PATCH 3/3] Use lock for loading all data. --- .../Internal/DataSources/FileDataSource.cs | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/pkgs/sdk/server/src/Internal/DataSources/FileDataSource.cs b/pkgs/sdk/server/src/Internal/DataSources/FileDataSource.cs index 0b97932b..91cb989b 100644 --- a/pkgs/sdk/server/src/Internal/DataSources/FileDataSource.cs +++ b/pkgs/sdk/server/src/Internal/DataSources/FileDataSource.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; @@ -23,10 +23,10 @@ internal sealed class FileDataSource : IDataSource private readonly FileDataTypes.IFileReader _fileReader; private readonly bool _skipMissingPaths; private readonly Logger _logger; - private readonly object _loadLock = new object(); private volatile bool _started; private volatile bool _loadedValidData; private volatile int _lastVersion; + private object _updateLock = new object(); public FileDataSource(IDataSourceUpdates dataSourceUpdates, FileDataTypes.IFileReader fileReader, List paths, bool autoUpdate, Func alternateParser, bool skipMissingPaths, @@ -89,7 +89,7 @@ private void Dispose(bool disposing) private void LoadAll() { - lock (_loadLock) + lock (_updateLock) { var version = Interlocked.Increment(ref _lastVersion); var flags = new Dictionary(); @@ -113,6 +113,7 @@ private void LoadAll() return; } } + var allData = new FullDataSet( ImmutableDictionary.Create>() .SetItem(DataModel.Features, new KeyedItems(flags)) @@ -187,15 +188,10 @@ internal sealed class FileWatchingReloader : IDisposable private readonly ISet _filePaths; private readonly Action _reload; private readonly List _watchers; - private readonly Timer _debounceTimer; - private readonly int _debounceMillis; - private readonly object _timerLock = new object(); - public FileWatchingReloader(List paths, Action reload, int debounceMillis = 100) + public FileWatchingReloader(List paths, Action reload) { _reload = reload; - _debounceMillis = debounceMillis; - _debounceTimer = new Timer(OnDebounceTimerElapsed, null, Timeout.Infinite, Timeout.Infinite); _filePaths = new HashSet(); var dirPaths = new HashSet(); @@ -225,19 +221,10 @@ private void ChangedPath(string path) { if (_filePaths.Contains(path)) { - lock (_timerLock) - { - // Reset the timer to debounce multiple rapid file changes - _debounceTimer.Change(_debounceMillis, Timeout.Infinite); - } + _reload(); } } - private void OnDebounceTimerElapsed(object state) - { - _reload(); - } - public void Dispose() { Dispose(true); @@ -247,7 +234,6 @@ private void Dispose(bool disposing) { if (disposing) { - _debounceTimer?.Dispose(); foreach (var w in _watchers) { w.Dispose();