From e8d744ecd405b97e317e1b1716db9b35934186cb Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Thu, 12 Mar 2026 14:20:36 -0700 Subject: [PATCH 1/4] try fix race condntion in cleanup --- src/Config/ConfigFileWatcher.cs | 26 ++++++++++++++++++- src/Config/FileSystemRuntimeConfigLoader.cs | 19 +++++++++++++- .../Configuration/ConfigurationTests.cs | 18 ++++++++++++- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/Config/ConfigFileWatcher.cs b/src/Config/ConfigFileWatcher.cs index 288a95e3d7..e596bc7701 100644 --- a/src/Config/ConfigFileWatcher.cs +++ b/src/Config/ConfigFileWatcher.cs @@ -20,8 +20,10 @@ namespace Azure.DataApiBuilder.Config; /// /// /// -public class ConfigFileWatcher +public class ConfigFileWatcher : IDisposable { + private bool _disposed; + /// /// Watches a specific file for modifications and alerts /// this class when a change is detected. @@ -120,4 +122,26 @@ private void OnConfigFileChange(object sender, FileSystemEventArgs e) Console.WriteLine("Unable to hot reload configuration file due to " + ex.Message); } } + + /// + /// Disposes the file watcher and unsubscribes from events to release + /// file handles and prevent further file change notifications. + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + if (_fileWatcher is not null) + { + _fileWatcher.EnableRaisingEvents = false; + _fileWatcher.Changed -= OnConfigFileChange; + _fileWatcher.Dispose(); + _fileWatcher = null; + } + } } diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index 70b36d8294..cee42cf341 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -30,8 +30,9 @@ namespace Azure.DataApiBuilder.Config; /// which allows for mocking of the file system in tests, providing a way to run the test /// in isolation of other tests or the actual file system. /// -public class FileSystemRuntimeConfigLoader : RuntimeConfigLoader +public class FileSystemRuntimeConfigLoader : RuntimeConfigLoader, IDisposable { + private bool _disposed; /// /// This stores either the default config name e.g. dab-config.json /// or user provided config file which could be a relative file path, @@ -91,6 +92,22 @@ public FileSystemRuntimeConfigLoader( _isCliLoader = isCliLoader; } + /// + /// Disposes the config file watcher to release file handles and stop + /// monitoring the config file for changes. + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _configFileWatcher?.Dispose(); + _configFileWatcher = null; + } + /// /// Get the directory name of the config file and /// return as a string. diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 0ef9b67a4b..b503bc9f23 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -707,9 +707,25 @@ type Moon { [TestCleanup] public void CleanupAfterEachTest() { + // Retry file deletion with exponential back-off to handle cases where a + // file watcher or hot-reload process may still hold a handle on the file. if (File.Exists(CUSTOM_CONFIG_FILENAME)) { - File.Delete(CUSTOM_CONFIG_FILENAME); + int retryCount = 0; + const int maxRetries = 3; + while (true) + { + try + { + File.Delete(CUSTOM_CONFIG_FILENAME); + break; + } + catch (IOException) when (retryCount < maxRetries) + { + retryCount++; + Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, retryCount))); + } + } } TestHelper.UnsetAllDABEnvironmentVariables(); From 70397af4a53afca53d3cac79b0644b16f4c64288 Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:32:01 -0700 Subject: [PATCH 2/4] Update src/Config/FileSystemRuntimeConfigLoader.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Config/FileSystemRuntimeConfigLoader.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index cee42cf341..2f4dff28c4 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -104,8 +104,13 @@ public void Dispose() } _disposed = true; - _configFileWatcher?.Dispose(); - _configFileWatcher = null; + + if (_configFileWatcher is not null) + { + _configFileWatcher.NewFileContentsDetected -= OnNewFileContentsDetected; + _configFileWatcher.Dispose(); + _configFileWatcher = null; + } } /// From b4b097975350a2e9f564693d342cd1e9a930b348 Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:33:20 -0700 Subject: [PATCH 3/4] Update src/Config/ConfigFileWatcher.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Config/ConfigFileWatcher.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Config/ConfigFileWatcher.cs b/src/Config/ConfigFileWatcher.cs index e596bc7701..e1afb39838 100644 --- a/src/Config/ConfigFileWatcher.cs +++ b/src/Config/ConfigFileWatcher.cs @@ -141,7 +141,6 @@ public void Dispose() _fileWatcher.EnableRaisingEvents = false; _fileWatcher.Changed -= OnConfigFileChange; _fileWatcher.Dispose(); - _fileWatcher = null; } } } From aa0a5a12a4ab11d9e55f68f27630ac64e4d1bb17 Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Thu, 12 Mar 2026 23:24:07 -0700 Subject: [PATCH 4/4] add some logging for clarity --- src/Service.Tests/Configuration/ConfigurationTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 094a2a556e..e328ddca84 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -723,9 +723,10 @@ public void CleanupAfterEachTest() File.Delete(CUSTOM_CONFIG_FILENAME); break; } - catch (IOException) when (retryCount < maxRetries) + catch (IOException ex) when (retryCount < maxRetries) { retryCount++; + Console.WriteLine($"CleanupAfterEachTest: Retry {retryCount}/{maxRetries} deleting {CUSTOM_CONFIG_FILENAME}. {ex.Message}"); Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, retryCount))); } }