From d41afdd4062bc5b1116c0440a6531f3fc28833b5 Mon Sep 17 00:00:00 2001 From: Oleksii Datsiuk <58850773+oleksii-datsiuk@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:42:19 +0300 Subject: [PATCH 1/3] Fix ENSURE: empty page must be defined as empty type --- LiteDB/Engine/Services/SnapShot.cs | 18 ++++---- LiteDB/Engine/Services/TransactionService.cs | 45 ++++++++++---------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/LiteDB/Engine/Services/SnapShot.cs b/LiteDB/Engine/Services/SnapShot.cs index f8deaeba7..9d37a5667 100644 --- a/LiteDB/Engine/Services/SnapShot.cs +++ b/LiteDB/Engine/Services/SnapShot.cs @@ -159,18 +159,18 @@ public void Dispose() #region Page Version functions /// - /// Get a a valid page for this snapshot (must consider local-index and wal-index) + /// Get a valid page for this snapshot (must consider local-index and wal-index) /// - public T GetPage(uint pageID) + public T GetPage(uint pageID, bool useLatestVersion = false) where T : BasePage { - return this.GetPage(pageID, out var origin, out var position, out var walVersion); + return this.GetPage(pageID, out var origin, out var position, out var walVersion, useLatestVersion); } /// - /// Get a a valid page for this snapshot (must consider local-index and wal-index) + /// Get a valid page for this snapshot (must consider local-index and wal-index) /// - public T GetPage(uint pageID, out FileOrigin origin, out long position, out int walVersion) + public T GetPage(uint pageID, out FileOrigin origin, out long position, out int walVersion, bool useLatestVersion = false) where T : BasePage { ENSURE(!_disposed, "the snapshot is disposed"); @@ -198,7 +198,7 @@ public T GetPage(uint pageID, out FileOrigin origin, out long position, out i } // if page is not in local cache, get from disk (log/wal/data) - page = this.ReadPage(pageID, out origin, out position, out walVersion); + page = this.ReadPage(pageID, out origin, out position, out walVersion, useLatestVersion); // add into local pages _localPages[pageID] = page; @@ -212,7 +212,7 @@ public T GetPage(uint pageID, out FileOrigin origin, out long position, out i /// /// Read page from disk (dirty, wal or data) /// - private T ReadPage(uint pageID, out FileOrigin origin, out long position, out int walVersion) + private T ReadPage(uint pageID, out FileOrigin origin, out long position, out int walVersion, bool useLatestVersion = false) where T : BasePage { // if not inside local pages can be a dirty page saved in log file @@ -232,7 +232,7 @@ private T ReadPage(uint pageID, out FileOrigin origin, out long position, out } // now, look inside wal-index - var pos = _walIndex.GetPageIndex(pageID, _readVersion, out walVersion); + var pos = _walIndex.GetPageIndex(pageID, useLatestVersion ? int.MaxValue : _readVersion, out walVersion); if (pos != long.MaxValue) { @@ -352,7 +352,7 @@ public T NewPage() // try get page from Empty free list if (_header.FreeEmptyPageList != uint.MaxValue) { - var free = this.GetPage(_header.FreeEmptyPageList); + var free = this.GetPage(_header.FreeEmptyPageList, useLatestVersion: true); ENSURE(free.PageType == PageType.Empty, "empty page must be defined as empty type"); diff --git a/LiteDB/Engine/Services/TransactionService.cs b/LiteDB/Engine/Services/TransactionService.cs index 31c2a6e42..b01b40c18 100644 --- a/LiteDB/Engine/Services/TransactionService.cs +++ b/LiteDB/Engine/Services/TransactionService.cs @@ -205,29 +205,25 @@ IEnumerable source() // in commit with header page change, last page will be header if (commit && _transPages.HeaderChanged) { - lock(_header) - { - // update this confirm page with current transactionID - _header.TransactionID = _transactionID; + // update this confirm page with current transactionID + _header.TransactionID = _transactionID; - // this header page will be marked as confirmed page in log file - _header.IsConfirmed = true; + // this header page will be marked as confirmed page in log file + _header.IsConfirmed = true; - // invoke all header callbacks (new/drop collections) - _transPages.OnCommit(_header); + // invoke all header callbacks (new/drop collections) + _transPages.OnCommit(_header); - // clone header page - var buffer = _header.UpdateBuffer(); - var clone = _disk.NewPage(); + // clone header page + var buffer = _header.UpdateBuffer(); + var clone = _disk.NewPage(); - // mem copy from current header to new header clone - Buffer.BlockCopy(buffer.Array, buffer.Offset, clone.Array, clone.Offset, clone.Count); + // mem copy from current header to new header clone + Buffer.BlockCopy(buffer.Array, buffer.Offset, clone.Array, clone.Offset, clone.Count); - // persist header in log file - yield return clone; - } + // persist header in log file + yield return clone; } - }; // write all dirty pages, in sequence on log-file and store references into log pages on transPages @@ -256,13 +252,16 @@ public void Commit() if (_mode == LockMode.Write || _transPages.HeaderChanged) { - // persist all dirty page as commit mode (mark last page as IsConfirm) - var count = this.PersistDirtyPages(true); - - // update wal-index (if any page was added into log disk) - if(count > 0) + lock (_header) { - _walIndex.ConfirmTransaction(_transactionID, _transPages.DirtyPages.Values); + // persist all dirty page as commit mode (mark last page as IsConfirm) + var count = this.PersistDirtyPages(true); + + // update wal-index (if any page was added into log disk) + if (count > 0) + { + _walIndex.ConfirmTransaction(_transactionID, _transPages.DirtyPages.Values); + } } } From 142376ea1cf560ebf41a4023718df2d3f85796d9 Mon Sep 17 00:00:00 2001 From: Oleksii Datsiuk <58850773+oleksii-datsiuk@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:44:33 +0300 Subject: [PATCH 2/3] Add savepoint for header where it seems to be intended (according to comment in the code) but minimize scope of the code affected by savepoint --- LiteDB/Engine/Services/SnapShot.cs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/LiteDB/Engine/Services/SnapShot.cs b/LiteDB/Engine/Services/SnapShot.cs index 9d37a5667..f34840e2f 100644 --- a/LiteDB/Engine/Services/SnapShot.cs +++ b/LiteDB/Engine/Services/SnapShot.cs @@ -375,11 +375,21 @@ public T NewPage() if (newLength > _header.Pragmas.LimitSize) throw new LiteException(0, $"Maximum data file size has been reached: {FileHelper.FormatFileSize(_header.Pragmas.LimitSize)}"); - // increase LastPageID from shared page - pageID = ++_header.LastPageID; - - // request for a new buffer - buffer = _reader.NewPage(); + var savepoint = _header.Savepoint(); + try + { + // increase LastPageID from shared page + pageID = ++_header.LastPageID; + + // request for a new buffer + buffer = _reader.NewPage(); + } + catch + { + // must revert all header content if any error occurs during header change + _header.Restore(savepoint); + throw; + } } // retain a list of created pages to, in a rollback situation, back pages to empty list From 73f586e3ab4e2565edb1951e05f55685af5136c0 Mon Sep 17 00:00:00 2001 From: Oleksii Datsiuk <58850773+oleksii-datsiuk@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:48:28 +0300 Subject: [PATCH 3/3] Make WalIndexService.CurrentReadVersion property synchronized with other functions that modify its backing field --- LiteDB/Engine/Services/WalIndexService.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/LiteDB/Engine/Services/WalIndexService.cs b/LiteDB/Engine/Services/WalIndexService.cs index b17d9da92..eaccc1fe2 100644 --- a/LiteDB/Engine/Services/WalIndexService.cs +++ b/LiteDB/Engine/Services/WalIndexService.cs @@ -38,7 +38,22 @@ public WalIndexService(DiskService disk, LockService locker) /// /// Get current read version for all new transactions /// - public int CurrentReadVersion => _currentReadVersion; + public int CurrentReadVersion + { + get + { + _indexLock.TryEnterReadLock(-1); + + try + { + return _currentReadVersion; + } + finally + { + _indexLock.ExitReadLock(); + } + } + } /// /// Get current counter for transaction ID