From 2fd2acc35a49fb66d78aebeb5e7eaee5be74f3ac Mon Sep 17 00:00:00 2001 From: Ahmed-Yaseen99 <186510963+Ahmed-Yaseen99@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:02:01 +0000 Subject: [PATCH] perf: replace unconditional Thread.Sleep(500) with polling retry in LocalDatabase Replaced the coarse Thread.Sleep(500) block in LocalDatabase exception handling with fine-grained 10ms polling retry mechanism (OpenDatabaseWithRetry). This allows database initialization to recover immediately when transient file locks clear early. --- src/NodeRadarPro/Data/LocalDatabase.cs | 43 ++++++++++++++----- .../Data/LocalDatabaseTests.cs | 36 ++++++++++++++++ 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/NodeRadarPro/Data/LocalDatabase.cs b/src/NodeRadarPro/Data/LocalDatabase.cs index dd4107d..509d908 100644 --- a/src/NodeRadarPro/Data/LocalDatabase.cs +++ b/src/NodeRadarPro/Data/LocalDatabase.cs @@ -95,16 +95,8 @@ public LocalDatabase(string? customDbPath = null, string? customPassword = null) } catch (IOException) { - // Transient file lock (antivirus, backup process) — retry once after backoff - System.Threading.Thread.Sleep(500); - try - { - _db = new LiteDatabase($"Filename={_dbPath};Password={_dbPassword};Connection=shared;"); - } - catch - { - _db = new LiteDatabase(new MemoryStream()); - } + // Transient file lock (antivirus, backup process) — retry with fine-grained backoff + _db = OpenDatabaseWithRetry(_dbPath, _dbPassword); } catch { @@ -115,6 +107,37 @@ public LocalDatabase(string? customDbPath = null, string? customPassword = null) } } + + private static LiteDatabase OpenDatabaseWithRetry(string dbPath, string dbPassword, int timeoutMs = 500, int pollIntervalMs = 10) + { + var sw = System.Diagnostics.Stopwatch.StartNew(); + while (sw.ElapsedMilliseconds < timeoutMs) + { + System.Threading.Thread.Sleep(pollIntervalMs); + try + { + return new LiteDatabase($"Filename={dbPath};Password={dbPassword};Connection=shared;"); + } + catch (IOException) + { + // Continue retry polling until timeout + } + catch + { + return new LiteDatabase(new MemoryStream()); + } + } + + try + { + return new LiteDatabase($"Filename={dbPath};Password={dbPassword};Connection=shared;"); + } + catch + { + return new LiteDatabase(new MemoryStream()); + } + } + internal void DisposeConnection() { lock (SyncRoot) diff --git a/tests/NodeRadarPro.Tests/Data/LocalDatabaseTests.cs b/tests/NodeRadarPro.Tests/Data/LocalDatabaseTests.cs index 3c221b0..05235cb 100644 --- a/tests/NodeRadarPro.Tests/Data/LocalDatabaseTests.cs +++ b/tests/NodeRadarPro.Tests/Data/LocalDatabaseTests.cs @@ -753,4 +753,40 @@ public void PruneOldData_DefaultArguments_UsesSettingsValues() Assert.DoesNotContain(remaining, l => l.Message == "Expired Log"); Assert.Contains(remaining, l => l.Message == "Valid Log"); } + [Fact] + public void DefaultConstructor_WhenTransientIOExceptionIsResolvedQuickly_RecoversFast() + { + string myDocuments = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + string myFolder = Path.Combine(myDocuments, "PyPie Studio", "NodeRadar Pro"); + Directory.CreateDirectory(myFolder); + string dbPath = Path.Combine(myFolder, "noderadar.db"); + + using var readyEvt = new ManualResetEventSlim(false); + using var releaseEvt = new ManualResetEventSlim(false); + + var lockTask = Task.Run(() => + { + using var fs = new FileStream(dbPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + readyEvt.Set(); + releaseEvt.Wait(3000); + }); + + readyEvt.Wait(1000); + + // Schedule lock release after 20ms + _ = Task.Run(async () => + { + await Task.Delay(20); + releaseEvt.Set(); + }); + + var sw = System.Diagnostics.Stopwatch.StartNew(); + using var db = new LocalDatabase(); + sw.Stop(); + + lockTask.Wait(1000); + + Assert.NotNull(db); + Assert.True(sw.ElapsedMilliseconds < 450, $"Expected recovery in < 450ms, took {sw.ElapsedMilliseconds} ms"); + } }