-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Fix/auto heal corrupt wal free list #2743
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.IO.Compression; | ||
| using FluentAssertions; | ||
| using Xunit; | ||
|
|
||
| namespace LiteDB.Tests.Issues | ||
| { | ||
| public class Issue1940_Tests | ||
| { | ||
| [Fact] | ||
| public void OpeningDatabaseWithLegacyCorruptFreeListInWalShouldAutoHeal() | ||
| { | ||
| var tempDirectory = Path.Combine(Path.GetTempPath(), $"litedb-issue1940-{Guid.NewGuid():N}"); | ||
| Directory.CreateDirectory(tempDirectory); | ||
|
|
||
| try | ||
| { | ||
| ZipFile.ExtractToDirectory( | ||
| Path.Combine(AppContext.BaseDirectory, "Resources", "Issue1940_CorruptFreeEmptyList.zip"), | ||
| tempDirectory); | ||
|
|
||
| var databasePath = Path.Combine(tempDirectory, "Issue1940_CorruptFreeEmptyList.db"); | ||
| var logPath = Path.Combine(tempDirectory, "Issue1940_CorruptFreeEmptyList-log.db"); | ||
|
|
||
| File.Exists(databasePath).Should().BeTrue(); | ||
| File.Exists(logPath).Should().BeTrue(); | ||
|
|
||
| Action firstOpen = () => | ||
| { | ||
| using var db = new LiteDatabase(databasePath); | ||
| var col = db.GetCollection<LegacyCorruptWalDoc>("verify"); | ||
|
|
||
| col.EnsureIndex(x => x.Tags); | ||
| col.Insert(this.CreateDocs()); | ||
|
|
||
| db.Checkpoint(); | ||
| }; | ||
|
|
||
| firstOpen.Should().NotThrow(); | ||
|
|
||
| if (File.Exists(logPath)) | ||
| { | ||
| new FileInfo(logPath).Length.Should().Be(0); | ||
| } | ||
|
|
||
| Action secondOpen = () => | ||
| { | ||
| using var db = new LiteDatabase(databasePath); | ||
| var col = db.GetCollection<LegacyCorruptWalDoc>("verify"); | ||
|
|
||
| col.Insert(this.CreateDocs()); | ||
| }; | ||
|
|
||
| secondOpen.Should().NotThrow(); | ||
| } | ||
| finally | ||
| { | ||
| if (Directory.Exists(tempDirectory)) | ||
| { | ||
| Directory.Delete(tempDirectory, true); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private IEnumerable<LegacyCorruptWalDoc> CreateDocs() | ||
| { | ||
| yield return new LegacyCorruptWalDoc | ||
| { | ||
| Number = 1, | ||
| Payload = new string('a', 2048), | ||
| Tags = new List<string> { "alpha", "beta" }, | ||
| Values = new List<string> { "one", "two" } | ||
| }; | ||
|
|
||
| yield return new LegacyCorruptWalDoc | ||
| { | ||
| Number = 2, | ||
| Payload = new string('b', 1536), | ||
| Tags = new List<string> { "gamma", "delta" }, | ||
| Values = new List<string> { "three", "four" } | ||
| }; | ||
| } | ||
|
|
||
| private class LegacyCorruptWalDoc | ||
| { | ||
| public ObjectId Id { get; set; } = ObjectId.NewObjectId(); | ||
|
|
||
| public int Number { get; set; } | ||
|
|
||
| public string Payload { get; set; } = string.Empty; | ||
|
|
||
| public List<string> Tags { get; set; } = new List<string>(); | ||
|
|
||
| public List<string> Values { get; set; } = new List<string>(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,109 @@ namespace LiteDB.Engine | |
| { | ||
| public partial class LiteEngine | ||
| { | ||
| private void HealCorruptedFreeEmptyPageList() | ||
| { | ||
| if (_header.FreeEmptyPageList == uint.MaxValue) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| using var reader = _disk.GetReader(); | ||
|
|
||
| var current = _header.FreeEmptyPageList; | ||
| var visited = new HashSet<uint>(); | ||
|
|
||
| while (current != uint.MaxValue) | ||
| { | ||
| if (current > _header.LastPageID || visited.Add(current) == false) | ||
| { | ||
| this.RepairFreeEmptyPageList(current, null); | ||
| return; | ||
| } | ||
|
|
||
| var page = this.ReadLatestPage(current, reader); | ||
|
|
||
| try | ||
| { | ||
| if (page.PageType != PageType.Empty) | ||
| { | ||
| this.RepairFreeEmptyPageList(current, page.PageType); | ||
| return; | ||
| } | ||
|
|
||
| current = page.NextPageID; | ||
| } | ||
| finally | ||
| { | ||
| page.Buffer.Release(); | ||
| } | ||
| } | ||
|
Comment on lines
+33
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unreadable pages still bypass the heal path. Because the Also applies to: 52-61 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| private BasePage ReadLatestPage(uint pageID, DiskReader reader) | ||
| { | ||
| var position = _walIndex.GetPageIndex(pageID, int.MaxValue, out _); | ||
|
|
||
| if (position != long.MaxValue) | ||
| { | ||
| return new BasePage(reader.ReadPage(position, false, FileOrigin.Log)); | ||
| } | ||
|
|
||
| return new BasePage(reader.ReadPage(BasePage.GetPagePosition(pageID), false, FileOrigin.Data)); | ||
| } | ||
|
|
||
| private void RepairFreeEmptyPageList(uint pageID, PageType? pageType) | ||
| { | ||
| LOG( | ||
| pageType.HasValue | ||
| ? $"detected legacy corruption in free empty page list at page {pageID} ({pageType.Value}); resetting header free list" | ||
| : $"detected legacy corruption in free empty page list near page {pageID}; resetting header free list", | ||
| "RECOVERY"); | ||
|
|
||
| lock (_header) | ||
| { | ||
| var savepoint = _header.Savepoint(); | ||
|
|
||
| try | ||
| { | ||
| _header.FreeEmptyPageList = uint.MaxValue; | ||
| _header.TransactionID = uint.MaxValue; | ||
| _header.IsConfirmed = false; | ||
| _header.UpdateBuffer(); | ||
|
|
||
| if (_settings.ReadOnly == false) | ||
| { | ||
| this.PersistRecoveredHeader(); | ||
| } | ||
| } | ||
| catch | ||
| { | ||
| _header.Restore(savepoint); | ||
| throw; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private void PersistRecoveredHeader() | ||
| { | ||
| var transactionID = _walIndex.NextTransactionID(); | ||
|
|
||
| _header.TransactionID = transactionID; | ||
| _header.IsConfirmed = true; | ||
|
|
||
| var buffer = _header.UpdateBuffer(); | ||
| var clone = _disk.NewPage(); | ||
|
|
||
| Buffer.BlockCopy(buffer.Array, buffer.Offset, clone.Array, clone.Offset, clone.Count); | ||
|
|
||
| _disk.WriteLogDisk(new[] { clone }); | ||
| _walIndex.ConfirmTransaction(transactionID, new[] { new PagePosition(0, clone.Position) }); | ||
|
|
||
| _header.TransactionID = uint.MaxValue; | ||
| _header.IsConfirmed = false; | ||
| _header.UpdateBuffer(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Recovery datafile using a rebuild process. Run only on "Open" database | ||
| /// </summary> | ||
|
|
@@ -28,4 +131,4 @@ private void Recovery(Collation collation) | |
| rebuilder.Rebuild(options); | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert that the corrupt fixture was really extracted.
LiteEngine.Open()only enters the new heal path when the WAL exists, but this test never proves thatissue1940.dbandissue1940-log.dbcame out of the archive. If the zip layout regresses,new LiteDatabase(databasePath)just creates a clean database and this still passes.Add explicit fixture checks right after extraction
ZipFile.ExtractToDirectory( Path.Combine(AppContext.BaseDirectory, "Resources", "Issue1940_CorruptFreeEmptyList.zip"), tempDirectory); + + File.Exists(databasePath).Should().BeTrue("the corrupt datafile fixture must be present"); + File.Exists(logPath).Should().BeTrue("the auto-heal path only runs when the WAL fixture is present"); + new FileInfo(logPath).Length.Should().BeGreaterThan(0, "the fixture should start with a non-empty WAL");Also applies to: 40-43
🤖 Prompt for AI Agents