Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,5 @@ FakesAssemblies/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
/LiteDB.BadJsonTest
/LiteDB/Properties/PublishProfiles/FolderProfile.pubxml
/LiteDB/README.md
112 changes: 112 additions & 0 deletions LiteDB.Tests/Issues/Issue2504_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System;
using System.IO;
using System.Linq;

using Xunit;

namespace LiteDB.Tests.Issues
{
public class Person
{
[BsonId]
public int Id { get; set; }
public string Name { get; set; }

public Person(int id, string name)
{
Id = id;
Name = name;
}
}

public class Issue2504_Tests
{
private static string CreateCorruptedDatabase()
{
using var memoryStream = new MemoryStream();
using (var db = new LiteDatabase(memoryStream))
{
var col1 = db.GetCollection<Person>("col1");
col1.Insert(new Person(1, "Alpha"));
var col2 = db.GetCollection<Person>("col2");
col2.Insert(new Person(2, "Beta"));
db.DropCollection("col2");
}

// 2) Zmień typ wszystkich pustych stron na Data (4)
var bytes = memoryStream.ToArray();
const int pageSize = 8192;
const int PAGE_TYPE_OFFSET = 4;
const byte PAGE_TYPE_EMPTY = 0;
const byte PAGE_TYPE_DATA = 4;

for (int offset = 0; offset + pageSize <= bytes.Length; offset += pageSize)
{
if (bytes[offset + PAGE_TYPE_OFFSET] == PAGE_TYPE_EMPTY)
{
bytes[offset + PAGE_TYPE_OFFSET] = PAGE_TYPE_DATA;
}
}

var tempPath = Path.Combine(Path.GetTempPath(), $"LiteDB_Issue2504_{Guid.NewGuid():N}.db");
File.WriteAllBytes(tempPath, bytes);
return tempPath;
}

[Fact]
public void AutoRebuild_Disabled_ShouldThrow()
{
var dbPath = CreateCorruptedDatabase();
var backupPath = dbPath + "-backup";

try
{
using var db = new LiteDatabase(dbPath);
var col1 = db.GetCollection<Person>("col1");
var bulk = Enumerable.Range(3, 5_000).Select(i => new Person(i, "Gamma"));
var ex = Record.Exception(() => col1.InsertBulk(bulk));
Assert.NotNull(ex);
Assert.False(File.Exists(backupPath));
}
finally
{
if (File.Exists(backupPath)) File.Delete(backupPath);
if (File.Exists(dbPath)) File.Delete(dbPath);
}
}

[Fact]
public void AutoRebuild_Enabled_ShouldRecover()
{
string dbPath = CreateCorruptedDatabase();
string backupPath = dbPath + "-backup";
try
{
using (var db = new LiteDatabase($"Filename={dbPath};AutoRebuild=true"))
{
var col1 = db.GetCollection<Person>("col1");

var bulk = Enumerable.Range(3, 500).Select(i => new Person(i, "Gamma"));
col1.InsertBulk(bulk);

var allDocs = col1.Query().ToList();
Assert.Contains(allDocs, x => x.Name == "Alpha");
Assert.True(allDocs.Count >= 2);
Assert.False(db.CollectionExists("col2"));
if (db.CollectionExists("_rebuild_errors"))
{
var rebuildErrors = db.GetCollection<BsonDocument>("_rebuild_errors");
Assert.True(rebuildErrors.Count() > 0, "Rebuild errors should be logged due to corruption");
}
}
Assert.True(File.Exists(backupPath), "Backup should exist when AutoRebuild has executed");

}
finally
{
if (File.Exists(backupPath)) File.Delete(backupPath);
if (File.Exists(dbPath)) File.Delete(dbPath);
}
}
}
}
157 changes: 157 additions & 0 deletions LiteDB.Tests/Issues/Issue2525_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LiteDB;
using LiteDB.Engine;
using Xunit;

namespace LiteDB.Tests.Issues
{
public class Issue2525_Tests
{
private static string NewTempDbPath()
=> Path.Combine(Path.GetTempPath(), $"LiteDB_FindAllLoop_{Guid.NewGuid():N}.db");

private static LiteEngine NewEngine(string path)
=> new LiteEngine(new EngineSettings { Filename = path });

private static void InsertPeopleWithEngine(LiteEngine engine, string collection, IEnumerable<(int id, string name)> rows)
{
var docs = rows.Select(r => new BsonDocument { ["_id"] = r.id, ["name"] = r.name });
engine.Insert(collection, docs, BsonAutoId.Int32);
}

private static void CreateIndexSelfLoop(LiteEngine engine, string collection, string indexName)
{
engine.BeginTrans();
var monitorField = typeof(LiteEngine).GetField("_monitor", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var monitor = (TransactionMonitor)monitorField.GetValue(engine);

var tx = monitor.GetTransaction(create: false, queryOnly: false, out _);
Assert.NotNull(tx);

var snapshot = tx.CreateSnapshot(LockMode.Write, collection, addIfNotExists: false);
Assert.NotNull(snapshot);
Assert.NotNull(snapshot.CollectionPage);

CollectionIndex ci = indexName == "_id"
? snapshot.CollectionPage.PK
: snapshot.CollectionPage.GetCollectionIndex(indexName);

Assert.NotNull(ci);

var headPage = snapshot.GetPage<IndexPage>(ci.Head.PageID);
var headNode = headPage.GetIndexNode(ci.Head.Index);
var firstAddr = headNode.Next[0];
Assert.False(firstAddr.IsEmpty);
var firstPage = snapshot.GetPage<IndexPage>(firstAddr.PageID);
var firstNode = firstPage.GetIndexNode(firstAddr.Index);
firstNode.SetNext(0, firstNode.Position);
tx.Commit();
}

[Fact]
public void PK_Loop_Should_Throw_On_EnsureIndex()
{
var path = NewTempDbPath();
try
{
using (var engine = NewEngine(path))
{
InsertPeopleWithEngine(engine, "col", new[]
{
(1, "a"),
(2, "b"),
(3, "c")
});
}

using (var engine = NewEngine(path))
{
CreateIndexSelfLoop(engine, "col", "_id");
}

using (var db = new LiteDatabase(path))
{
var col = db.GetCollection("col");
var ex = Record.Exception(() =>
{
col.EnsureIndex("name"); // albo Find(Query.All("name")), zależnie od testu
});

Assert.NotNull(ex);
Assert.Contains("Detected loop in FindAll", ex.Message, StringComparison.OrdinalIgnoreCase);

}
}
finally
{
if (File.Exists(path)) File.Delete(path);
}
}

[Fact]
public void Secondary_Index_Loop_Should_Throw_On_Query_Then_Rebuild_Fixes()
{
var path = NewTempDbPath();
var backup = path + "-backup";
try
{
using (var engine = NewEngine(path))
{
InsertPeopleWithEngine(engine, "col", new[]
{
(1, "a"),
(2, "b"),
(3, "c")
});
}

using (var db = new LiteDatabase(path))
{
var col = db.GetCollection("col");
col.EnsureIndex("name");
}

using (var engine = NewEngine(path))
{
CreateIndexSelfLoop(engine, "col", "name");
}

using (var db = new LiteDatabase(path))
{
var ex = Record.Exception(() =>
{
var _ = db.GetCollection("col").Query().OrderBy("name").ToList();
});

Assert.NotNull(ex);
Assert.Contains("Detected loop in FindAll", ex.Message, StringComparison.OrdinalIgnoreCase);

}

using (var db = new LiteDatabase(path))
{
db.Rebuild();
}

using (var db = new LiteDatabase(path))
{
var col = db.GetCollection("col");
var list = col.Find(Query.All("name")).ToList();

Assert.Equal(3, list.Count);
Assert.Equal(new[] { "a", "b", "c" }, list.Select(x => x["name"].AsString).OrderBy(x => x).ToArray());
}

if (File.Exists(backup)) File.Delete(backup);
}
finally
{
if (File.Exists(path)) File.Delete(path);
if (File.Exists(backup)) File.Delete(backup);
}
}
}
}
8 changes: 7 additions & 1 deletion LiteDB/Client/Structures/ConnectionString.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using LiteDB.Engine;

using System;
using System.Collections.Generic;
using System.Globalization;

using static LiteDB.Constants;

namespace LiteDB
Expand Down Expand Up @@ -85,7 +87,7 @@ public ConnectionString(string connectionString)

this.Password = _values.GetValue("password", this.Password);

if(this.Password == string.Empty)
if (this.Password == string.Empty)
{
this.Password = null;
}
Expand All @@ -96,6 +98,10 @@ public ConnectionString(string connectionString)
this.Collation = _values.ContainsKey("collation") ? new Collation(_values.GetValue<string>("collation")) : this.Collation;

this.Upgrade = _values.GetValue("upgrade", this.Upgrade);

if (_values.TryGetValue("autorebuild", out var v) && !_values.ContainsKey("auto-rebuild"))
_values["auto-rebuild"] = v;

this.AutoRebuild = _values.GetValue("auto-rebuild", this.AutoRebuild);
}

Expand Down
24 changes: 17 additions & 7 deletions LiteDB/Engine/Disk/DiskService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;

using LiteDB.Utils;

using static LiteDB.Constants;

namespace LiteDB.Engine
Expand Down Expand Up @@ -54,8 +57,15 @@ public DiskService(
if (isNew)
{
LOG($"creating new database: '{Path.GetFileName(_dataFactory.Name)}'", "DISK");

this.Initialize(_dataPool.Writer.Value, settings.Collation, settings.InitialSize);
try
{
this.Initialize(_dataPool.Writer.Value, settings.Collation, settings.InitialSize);
}
catch (Exception ex)
{
LOG($"Error while initializing DiskService: {ex.Message}", "ERROR");
throw;
}
}

// if not readonly, force open writable datafile
Expand Down Expand Up @@ -340,14 +350,14 @@ public void Dispose()
// can change file size
var delete = _logFactory.Exists() && _logPool.Writer.Value.Length == 0;

// dispose Stream pools
_dataPool.Dispose();
_logPool.Dispose();
var tc = new TryCatch();
tc.Catch(_dataPool.Dispose);
tc.Catch(_logPool.Dispose);

if (delete) _logFactory.Delete();
if (delete) tc.Catch(_logFactory.Delete);

// other disposes
_cache.Dispose();
tc.Catch(_cache.Dispose);
}
}
}
43 changes: 43 additions & 0 deletions LiteDB/Engine/Engine/LiteEngine.AutoRebuild.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;

namespace LiteDB.Engine
{
public partial class LiteEngine
{
private DateTime _lastAutoRebuildUtc = DateTime.MinValue;
private static readonly TimeSpan _autoRebuildCooldown = TimeSpan.FromMinutes(5);

private bool TryAutoRebuild(Exception ex, bool viaOpen = false)
{
try
{
if (!_settings.AutoRebuild) return false;
if (!IsStructuralCorruption(ex)) return false;
if (_autoRebuildInProgress) return false;
if (DateTime.UtcNow - _lastAutoRebuildUtc < _autoRebuildCooldown) return false;

_autoRebuildInProgress = true;
_lastAutoRebuildUtc = DateTime.UtcNow;

try
{
if (viaOpen)
AutoRebuildAndReopenViaOpenPath();
else
AutoRebuildAndReopen();

return true;
}
finally
{
_autoRebuildInProgress = false;
}
}
catch
{
// nie wyciekaj wyjątków na zewnątrz – decyzja o rethrow zostaje w wyższym poziomie
return false;
}
}
}
}
Loading