Implemented another hot reload locking mechanism

This commit is contained in:
Thorsten Sommer 2025-06-09 13:53:00 +02:00
parent bc88746c61
commit c6d61a3de0
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
3 changed files with 82 additions and 3 deletions

View File

@ -42,6 +42,7 @@ public static partial class PluginFactory
var tempDownloadFile = Path.GetTempFileName(); var tempDownloadFile = Path.GetTempFileName();
try try
{ {
await LockHotReloadAsync();
using var httpClient = new HttpClient(); using var httpClient = new HttpClient();
var response = await httpClient.GetAsync(downloadUrl, cancellationToken); var response = await httpClient.GetAsync(downloadUrl, cancellationToken);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
@ -80,6 +81,8 @@ public static partial class PluginFactory
LOG.LogError(e, "Failed to delete the temporary download file."); LOG.LogError(e, "Failed to delete the temporary download file.");
} }
} }
UnlockHotReload();
} }
return true; return true;

View File

@ -46,12 +46,41 @@ public static partial class PluginFactory
try try
{ {
LOG.LogInformation($"File changed ({changeType}): {args.FullPath}. Reloading plugins..."); LOG.LogInformation($"File changed ({changeType}): {args.FullPath}. Reloading plugins...");
if (File.Exists(HOT_RELOAD_LOCK_FILE))
{
LOG.LogInformation("Hot reload lock file exists. Waiting for it to be released before proceeding with the reload.");
var lockFileCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var token = lockFileCancellationTokenSource.Token;
var waitTime = TimeSpan.FromSeconds(1);
while (File.Exists(HOT_RELOAD_LOCK_FILE) && !token.IsCancellationRequested)
{
try
{
LOG.LogDebug("Waiting for hot reload lock to be released...");
await Task.Delay(waitTime, token);
waitTime = TimeSpan.FromSeconds(Math.Min(waitTime.TotalSeconds * 2, 120)); // Exponential backoff with a cap
}
catch (TaskCanceledException)
{
// Case: The cancellation token was triggered, meaning the lock file is still present.
// We expect that something goes wrong. So, we try to delete the lock file:
LOG.LogWarning("Hot reload lock file still exists after 30 seconds. Attempting to delete it...");
UnlockHotReload();
break;
}
}
LOG.LogInformation("Hot reload lock file released. Proceeding with plugin reload.");
}
// Wait for parallel writes to finish:
await Task.Delay(TimeSpan.FromSeconds(3));
await LoadAll(); await LoadAll();
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.PLUGINS_RELOADED); await MessageBus.INSTANCE.SendMessage<bool>(null, Event.PLUGINS_RELOADED);
} }
catch(Exception e)
{
LOG.LogError(e, $"Error while reloading plugins after change in file '{args.FullPath}' with change type '{changeType}'.");
}
finally finally
{ {
HOT_RELOAD_SEMAPHORE.Release(); HOT_RELOAD_SEMAPHORE.Release();

View File

@ -13,6 +13,7 @@ public static partial class PluginFactory
private static string PLUGINS_ROOT = string.Empty; private static string PLUGINS_ROOT = string.Empty;
private static string INTERNAL_PLUGINS_ROOT = string.Empty; private static string INTERNAL_PLUGINS_ROOT = string.Empty;
private static string CONFIGURATION_PLUGINS_ROOT = string.Empty; private static string CONFIGURATION_PLUGINS_ROOT = string.Empty;
private static string HOT_RELOAD_LOCK_FILE = string.Empty;
private static FileSystemWatcher HOT_RELOAD_WATCHER = null!; private static FileSystemWatcher HOT_RELOAD_WATCHER = null!;
private static ILanguagePlugin BASE_LANGUAGE_PLUGIN = NoPluginLanguage.INSTANCE; private static ILanguagePlugin BASE_LANGUAGE_PLUGIN = NoPluginLanguage.INSTANCE;
@ -30,6 +31,7 @@ public static partial class PluginFactory
LOG.LogInformation("Initializing plugin factory..."); LOG.LogInformation("Initializing plugin factory...");
DATA_DIR = SettingsManager.DataDirectory!; DATA_DIR = SettingsManager.DataDirectory!;
PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins"); PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins");
HOT_RELOAD_LOCK_FILE = Path.Join(PLUGINS_ROOT, ".lock");
INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal"); INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal");
CONFIGURATION_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".config"); CONFIGURATION_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".config");
@ -42,6 +44,51 @@ public static partial class PluginFactory
return true; return true;
} }
private static async Task LockHotReloadAsync()
{
if (!IS_INITIALIZED)
{
LOG.LogError("PluginFactory is not initialized.");
return;
}
try
{
if (File.Exists(HOT_RELOAD_LOCK_FILE))
{
LOG.LogWarning("Hot reload lock file already exists.");
return;
}
await File.WriteAllTextAsync(HOT_RELOAD_LOCK_FILE, DateTime.UtcNow.ToString("o"));
}
catch (Exception e)
{
LOG.LogError(e, "An error occurred while trying to lock hot reloading.");
}
}
private static void UnlockHotReload()
{
if (!IS_INITIALIZED)
{
LOG.LogError("PluginFactory is not initialized.");
return;
}
try
{
if(File.Exists(HOT_RELOAD_LOCK_FILE))
File.Delete(HOT_RELOAD_LOCK_FILE);
else
LOG.LogWarning("Hot reload lock file does not exist. Nothing to unlock.");
}
catch (Exception e)
{
LOG.LogError(e, "An error occurred while trying to unlock hot reloading.");
}
}
public static void Dispose() public static void Dispose()
{ {
if(!IS_INITIALIZED) if(!IS_INITIALIZED)