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