Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions src/NodeRadarPro/Data/LocalDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions tests/NodeRadarPro.Tests/Data/LocalDatabaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Loading