diff --git a/src/Config/ConfigFileWatcher.cs b/src/Config/ConfigFileWatcher.cs index 288a95e3d7..e1afb39838 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,25 @@ 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(); + } + } } diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index ecefd6a9c2..7b888a82bf 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, @@ -102,6 +103,27 @@ public FileSystemRuntimeConfigLoader( _logBuffer = logBuffer; } + /// + /// 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; + + if (_configFileWatcher is not null) + { + _configFileWatcher.NewFileContentsDetected -= OnNewFileContentsDetected; + _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 323649baf8..26a0313e59 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -710,9 +710,26 @@ 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 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))); + } + } } TestHelper.UnsetAllDABEnvironmentVariables();