From dde694ff9f1fe1441cd9cc381be817873e3a6b9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:04:23 +0000 Subject: [PATCH 1/6] Initial plan From e647f459129a875024cb07a6f2acc91c727572ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:13:34 +0000 Subject: [PATCH 2/6] fix: dispose resources on DocumentDbContext constructor failure (issue #129) --- src/BLite.Core/DocumentDbContext.cs | 72 ++++++++++++++++++++-------- tests/BLite.Tests/EncryptionTests.cs | 37 ++++++++++++++ 2 files changed, 90 insertions(+), 19 deletions(-) diff --git a/src/BLite.Core/DocumentDbContext.cs b/src/BLite.Core/DocumentDbContext.cs index 04fd579..471e813 100644 --- a/src/BLite.Core/DocumentDbContext.cs +++ b/src/BLite.Core/DocumentDbContext.cs @@ -89,14 +89,25 @@ protected DocumentDbContext(string databasePath, PageFileConfig config, BLiteKvO _storage.RegisterCdc(_cdc); _freeSpaceIndexes = new FreeSpaceIndexProvider(_storage); _kvStore = new BLiteKvStore(_storage, kvOptions); + _model = new Dictionary(); - // Initialize model before collections - var modelBuilder = new ModelBuilder(); - OnModelCreating(modelBuilder); - _model = modelBuilder.GetEntityBuilders(); - InitializeCollections(); - DropOrphanCollections(); - RunGdprStrictValidation(kvOptions); + try + { + // Initialize model before collections + var modelBuilder = new ModelBuilder(); + OnModelCreating(modelBuilder); + _model = modelBuilder.GetEntityBuilders(); + InitializeCollections(); + DropOrphanCollections(); + RunGdprStrictValidation(kvOptions); + } + catch + { + _storage.Dispose(); + _cdc.Dispose(); + _disposed = true; + throw; + } } /// @@ -151,13 +162,25 @@ protected DocumentDbContext(string databasePath, CryptoOptions crypto, BLiteKvOp _storage.RegisterCdc(_cdc); _freeSpaceIndexes = new FreeSpaceIndexProvider(_storage); _kvStore = new BLiteKvStore(_storage, kvOptions); + _model = new Dictionary(); - var modelBuilder = new ModelBuilder(); - OnModelCreating(modelBuilder); - _model = modelBuilder.GetEntityBuilders(); - InitializeCollections(); - DropOrphanCollections(); - RunGdprStrictValidation(kvOptions); + try + { + var modelBuilder = new ModelBuilder(); + OnModelCreating(modelBuilder); + _model = modelBuilder.GetEntityBuilders(); + InitializeCollections(); + DropOrphanCollections(); + RunGdprStrictValidation(kvOptions); + } + catch + { + _storage.Dispose(); + _cdc.Dispose(); + _ownedCoordinator?.Dispose(); + _disposed = true; + throw; + } } /// @@ -199,13 +222,24 @@ protected DocumentDbContext(StorageEngine storage, BLiteKvOptions? kvOptions = n _storage.RegisterCdc(_cdc); _freeSpaceIndexes = new FreeSpaceIndexProvider(_storage); _kvStore = new BLiteKvStore(_storage, kvOptions); + _model = new Dictionary(); - var modelBuilder = new ModelBuilder(); - OnModelCreating(modelBuilder); - _model = modelBuilder.GetEntityBuilders(); - InitializeCollections(); - DropOrphanCollections(); - RunGdprStrictValidation(kvOptions); + try + { + var modelBuilder = new ModelBuilder(); + OnModelCreating(modelBuilder); + _model = modelBuilder.GetEntityBuilders(); + InitializeCollections(); + DropOrphanCollections(); + RunGdprStrictValidation(kvOptions); + } + catch + { + // _storage is caller-owned; do not dispose it here. + _cdc.Dispose(); + _disposed = true; + throw; + } } /// diff --git a/tests/BLite.Tests/EncryptionTests.cs b/tests/BLite.Tests/EncryptionTests.cs index 0f651b1..7d51e12 100644 --- a/tests/BLite.Tests/EncryptionTests.cs +++ b/tests/BLite.Tests/EncryptionTests.cs @@ -1577,4 +1577,41 @@ public async Task CryptoOptions_StringAndBytes_AreInteroperable() Assert.Equal("interop", e!.Payload); } } + + // ── DocumentDbContext constructor failure / file-lock regression (issue #129) ─ + + /// + /// Regression test for issue #129: + /// If the constructor fails (e.g. wrong encryption + /// password causing a decryption error), all acquired file handles must be released + /// before the exception propagates to the caller. + /// After the constructor throws, the database file must be deletable in the same + /// process without GC.Collect() or process restart. + /// + [Fact] + public async Task DocumentDbContext_ConstructorFailure_WrongPassword_FileNotLocked() + { + var path = TempDb(); + var correct = new CryptoOptions("correct-password", iterations: 1); + var wrong = new CryptoOptions("wrong-password", iterations: 1); + + // 1. Create the encrypted database and persist at least one document so that + // collection-metadata pages are written to the main file. + await using (var ctx = new MultiFileTestDbContext(path, correct)) + { + await ctx.Entries.InsertAsync(new MultiFileEntry { Id = 1, Payload = "hello", Tag = "t" }); + await ctx.CheckpointAsync(); // flush WAL into main file + } + + // 2. Attempt to open with the wrong password — the constructor must throw. + Assert.ThrowsAny(() => + { + using var ctx = new MultiFileTestDbContext(path, wrong); + }); + + // 3. The main database file must not be locked after the failed constructor. + // Before the fix this would throw IOException ("file is being used by another process"). + File.Delete(path); + Assert.False(File.Exists(path), "Database file must be deletable immediately after failed constructor."); + } } From 934c94e0a015ef18a1c697a3b29601e5f69cde1d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:19:17 +0000 Subject: [PATCH 3/6] fix: cleanup intermediate _model assignment and missing blank line --- src/BLite.Core/DocumentDbContext.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/BLite.Core/DocumentDbContext.cs b/src/BLite.Core/DocumentDbContext.cs index 471e813..ab9b32c 100644 --- a/src/BLite.Core/DocumentDbContext.cs +++ b/src/BLite.Core/DocumentDbContext.cs @@ -89,7 +89,6 @@ protected DocumentDbContext(string databasePath, PageFileConfig config, BLiteKvO _storage.RegisterCdc(_cdc); _freeSpaceIndexes = new FreeSpaceIndexProvider(_storage); _kvStore = new BLiteKvStore(_storage, kvOptions); - _model = new Dictionary(); try { @@ -162,7 +161,6 @@ protected DocumentDbContext(string databasePath, CryptoOptions crypto, BLiteKvOp _storage.RegisterCdc(_cdc); _freeSpaceIndexes = new FreeSpaceIndexProvider(_storage); _kvStore = new BLiteKvStore(_storage, kvOptions); - _model = new Dictionary(); try { @@ -222,7 +220,6 @@ protected DocumentDbContext(StorageEngine storage, BLiteKvOptions? kvOptions = n _storage.RegisterCdc(_cdc); _freeSpaceIndexes = new FreeSpaceIndexProvider(_storage); _kvStore = new BLiteKvStore(_storage, kvOptions); - _model = new Dictionary(); try { From 80cac7f2266973f70761c53484f58f7c8d34423b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:21:04 +0000 Subject: [PATCH 4/6] test: add pre-deletion existence assertion in constructor failure regression test --- tests/BLite.Tests/EncryptionTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/BLite.Tests/EncryptionTests.cs b/tests/BLite.Tests/EncryptionTests.cs index 7d51e12..621b360 100644 --- a/tests/BLite.Tests/EncryptionTests.cs +++ b/tests/BLite.Tests/EncryptionTests.cs @@ -1611,6 +1611,7 @@ public async Task DocumentDbContext_ConstructorFailure_WrongPassword_FileNotLock // 3. The main database file must not be locked after the failed constructor. // Before the fix this would throw IOException ("file is being used by another process"). + Assert.True(File.Exists(path), "Database file should exist before attempting deletion."); File.Delete(path); Assert.False(File.Exists(path), "Database file must be deletable immediately after failed constructor."); } From 57dde0e514f9bd39d40c20a150e2414f128979db Mon Sep 17 00:00:00 2001 From: MrDevRobot Date: Tue, 21 Jul 2026 23:14:10 +0200 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/BLite.Core/DocumentDbContext.cs | 30 ++++++++++++++-------------- tests/BLite.Tests/EncryptionTests.cs | 11 +++++----- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/BLite.Core/DocumentDbContext.cs b/src/BLite.Core/DocumentDbContext.cs index ab9b32c..4de9f9f 100644 --- a/src/BLite.Core/DocumentDbContext.cs +++ b/src/BLite.Core/DocumentDbContext.cs @@ -100,13 +100,13 @@ protected DocumentDbContext(string databasePath, PageFileConfig config, BLiteKvO DropOrphanCollections(); RunGdprStrictValidation(kvOptions); } - catch - { - _storage.Dispose(); - _cdc.Dispose(); - _disposed = true; - throw; - } +catch +{ + try { _storage.Dispose(); } catch { /* best-effort; preserve original exception */ } + try { _cdc.Dispose(); } catch { /* best-effort */ } + _disposed = true; + throw; +} } /// @@ -171,14 +171,14 @@ protected DocumentDbContext(string databasePath, CryptoOptions crypto, BLiteKvOp DropOrphanCollections(); RunGdprStrictValidation(kvOptions); } - catch - { - _storage.Dispose(); - _cdc.Dispose(); - _ownedCoordinator?.Dispose(); - _disposed = true; - throw; - } +catch +{ + try { _storage.Dispose(); } catch { /* best-effort; preserve original exception */ } + try { _cdc.Dispose(); } catch { /* best-effort */ } + try { _ownedCoordinator?.Dispose(); } catch { /* best-effort */ } + _disposed = true; + throw; +} } /// diff --git a/tests/BLite.Tests/EncryptionTests.cs b/tests/BLite.Tests/EncryptionTests.cs index 621b360..9a5f66f 100644 --- a/tests/BLite.Tests/EncryptionTests.cs +++ b/tests/BLite.Tests/EncryptionTests.cs @@ -1609,10 +1609,11 @@ public async Task DocumentDbContext_ConstructorFailure_WrongPassword_FileNotLock using var ctx = new MultiFileTestDbContext(path, wrong); }); - // 3. The main database file must not be locked after the failed constructor. - // Before the fix this would throw IOException ("file is being used by another process"). - Assert.True(File.Exists(path), "Database file should exist before attempting deletion."); - File.Delete(path); - Assert.False(File.Exists(path), "Database file must be deletable immediately after failed constructor."); +// 3. The main database file must not be locked after the failed constructor. +// Before the fix this would throw IOException (exclusive open fails on Windows; advisory-lock fails on Unix). +Assert.True(File.Exists(path), "Database file should exist before attempting deletion."); +using (var _ = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) { } +File.Delete(path); +Assert.False(File.Exists(path), "Database file must be deletable immediately after failed constructor."); } } From 5719af41afd5e02a38d385da3411e96474a74385 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:23:35 +0000 Subject: [PATCH 6/6] Fix storage constructor cleanup on open failure --- src/BLite.Core/DocumentDbContext.cs | 39 +++-- src/BLite.Core/Storage/StorageEngine.cs | 211 +++++++++++++----------- 2 files changed, 136 insertions(+), 114 deletions(-) diff --git a/src/BLite.Core/DocumentDbContext.cs b/src/BLite.Core/DocumentDbContext.cs index 4de9f9f..8d5a3d0 100644 --- a/src/BLite.Core/DocumentDbContext.cs +++ b/src/BLite.Core/DocumentDbContext.cs @@ -86,7 +86,6 @@ protected DocumentDbContext(string databasePath, PageFileConfig config, BLiteKvO _databasePath = databasePath; _storage = new StorageEngine(databasePath, config); _cdc = new CDC.ChangeStreamDispatcher(); - _storage.RegisterCdc(_cdc); _freeSpaceIndexes = new FreeSpaceIndexProvider(_storage); _kvStore = new BLiteKvStore(_storage, kvOptions); @@ -100,13 +99,15 @@ protected DocumentDbContext(string databasePath, PageFileConfig config, BLiteKvO DropOrphanCollections(); RunGdprStrictValidation(kvOptions); } -catch -{ - try { _storage.Dispose(); } catch { /* best-effort; preserve original exception */ } - try { _cdc.Dispose(); } catch { /* best-effort */ } - _disposed = true; - throw; -} + catch + { + try { _storage.Dispose(); } catch { /* best-effort; preserve original exception */ } + try { _cdc.Dispose(); } catch { /* best-effort */ } + _disposed = true; + throw; + } + + _storage.RegisterCdc(_cdc); } /// @@ -158,7 +159,6 @@ protected DocumentDbContext(string databasePath, CryptoOptions crypto, BLiteKvOp _storage = new StorageEngine(databasePath, config); _cdc = new CDC.ChangeStreamDispatcher(); - _storage.RegisterCdc(_cdc); _freeSpaceIndexes = new FreeSpaceIndexProvider(_storage); _kvStore = new BLiteKvStore(_storage, kvOptions); @@ -171,14 +171,16 @@ protected DocumentDbContext(string databasePath, CryptoOptions crypto, BLiteKvOp DropOrphanCollections(); RunGdprStrictValidation(kvOptions); } -catch -{ - try { _storage.Dispose(); } catch { /* best-effort; preserve original exception */ } - try { _cdc.Dispose(); } catch { /* best-effort */ } - try { _ownedCoordinator?.Dispose(); } catch { /* best-effort */ } - _disposed = true; - throw; -} + catch + { + try { _storage.Dispose(); } catch { /* best-effort; preserve original exception */ } + try { _cdc.Dispose(); } catch { /* best-effort */ } + try { _ownedCoordinator?.Dispose(); } catch { /* best-effort */ } + _disposed = true; + throw; + } + + _storage.RegisterCdc(_cdc); } /// @@ -217,7 +219,6 @@ protected DocumentDbContext(StorageEngine storage, BLiteKvOptions? kvOptions = n { _storage = storage ?? throw new ArgumentNullException(nameof(storage)); _cdc = new CDC.ChangeStreamDispatcher(); - _storage.RegisterCdc(_cdc); _freeSpaceIndexes = new FreeSpaceIndexProvider(_storage); _kvStore = new BLiteKvStore(_storage, kvOptions); @@ -237,6 +238,8 @@ protected DocumentDbContext(StorageEngine storage, BLiteKvOptions? kvOptions = n _disposed = true; throw; } + + _storage.RegisterCdc(_cdc); } /// diff --git a/src/BLite.Core/Storage/StorageEngine.cs b/src/BLite.Core/Storage/StorageEngine.cs index fa5ba5d..5c5eb99 100644 --- a/src/BLite.Core/Storage/StorageEngine.cs +++ b/src/BLite.Core/Storage/StorageEngine.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Threading.Channels; +using BLite.Core.Encryption; using BLite.Core.Transactions; namespace BLite.Core.Storage; @@ -117,105 +118,123 @@ public sealed partial class StorageEngine : IDisposable public StorageEngine(string databasePath, PageFileConfig config) { _config = config; + ICryptoProvider? walCryptoProvider = null; + try + { + // Use WalPath if specified, otherwise derive from databasePath (default behavior unchanged) + var walPath = config.WalPath ?? Path.ChangeExtension(databasePath, ".wal"); - // Use WalPath if specified, otherwise derive from databasePath (default behavior unchanged) - var walPath = config.WalPath ?? Path.ChangeExtension(databasePath, ".wal"); - - // Ensure WAL parent directory exists - var walDirectory = Path.GetDirectoryName(walPath); - if (!string.IsNullOrWhiteSpace(walDirectory)) - Directory.CreateDirectory(walDirectory); + // Ensure WAL parent directory exists + var walDirectory = Path.GetDirectoryName(walPath); + if (!string.IsNullOrWhiteSpace(walDirectory)) + Directory.CreateDirectory(walDirectory); - // Initialize storage infrastructure - _pageFile = new PageFile(databasePath, config); + // Initialize storage infrastructure + _pageFile = new PageFile(databasePath, config); - _pageFile.Open(); - // NOTE: If CryptoProvider is a coordinator-managed provider, opening the main file - // primes the coordinator's database salt, making CreateSiblingProvider available - // for index, WAL, and per-collection files. + _pageFile.Open(); + // NOTE: If CryptoProvider is a coordinator-managed provider, opening the main file + // primes the coordinator's database salt, making CreateSiblingProvider available + // for index, WAL, and per-collection files. - // Phase 3: open separate index file if configured - if (config.IndexFilePath != null) - { - var idxDirectory = Path.GetDirectoryName(config.IndexFilePath); - if (!string.IsNullOrWhiteSpace(idxDirectory)) - Directory.CreateDirectory(idxDirectory); - - // Derive a dedicated index-file provider so that index and data pages - // encrypted with the same pageId never share a key. - var idxConfig = config.CryptoProvider != null - ? AsStandaloneConfig(config) with { CryptoProvider = config.CryptoProvider.CreateSiblingProvider(2, 0) } - : AsStandaloneConfig(config); - - _indexFile = new PageFile(config.IndexFilePath, idxConfig); - _indexFile.Open(); - } + // Phase 3: open separate index file if configured + if (config.IndexFilePath != null) + { + var idxDirectory = Path.GetDirectoryName(config.IndexFilePath); + if (!string.IsNullOrWhiteSpace(idxDirectory)) + Directory.CreateDirectory(idxDirectory); + + // Derive a dedicated index-file provider so that index and data pages + // encrypted with the same pageId never share a key. + var idxConfig = config.CryptoProvider != null + ? AsStandaloneConfig(config) with { CryptoProvider = config.CryptoProvider.CreateSiblingProvider(2, 0) } + : AsStandaloneConfig(config); + + _indexFile = new PageFile(config.IndexFilePath, idxConfig); + _indexFile.Open(); + } - // Phase 4: initialize collection-per-file structures if configured - if (config.CollectionDataDirectory != null) - { - Directory.CreateDirectory(config.CollectionDataDirectory); - _collectionFiles = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); - _collectionNameToSlot = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - _collectionSlotToName = new Dictionary(); - _slotsFilePath = Path.Combine(config.CollectionDataDirectory, ".slots"); - LoadCollectionSlots(); - } + // Phase 4: initialize collection-per-file structures if configured + if (config.CollectionDataDirectory != null) + { + Directory.CreateDirectory(config.CollectionDataDirectory); + _collectionFiles = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); + _collectionNameToSlot = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + _collectionSlotToName = new Dictionary(); + _slotsFilePath = Path.Combine(config.CollectionDataDirectory, ".slots"); + LoadCollectionSlots(); + } - // When encryption is configured, derive a dedicated WAL provider from the main-file - // provider so that WAL records are always encrypted alongside the rest of the database. - var walCryptoProvider = config.CryptoProvider?.CreateSiblingProvider(3, 0); + // When encryption is configured, derive a dedicated WAL provider from the main-file + // provider so that WAL records are always encrypted alongside the rest of the database. + walCryptoProvider = config.CryptoProvider?.CreateSiblingProvider(3, 0); + + _wal = new WriteAheadLog(walPath, walCryptoProvider, config.LockTimeout.WriteTimeoutMs, config.AllowMultiProcessAccess); + walCryptoProvider = null; + _walCache = new ConcurrentDictionary>(); + _walIndex = new ConcurrentDictionary(); + _activeTransactions = new ConcurrentDictionary(); + _nextTransactionId = 0; // Interlocked.Increment pre-increments, so first txnId == 1. + + // ── Multi-process WAL coordination (opt-in) ───────────────────────── + // When AllowMultiProcessAccess is true, open the .wal-shm sidecar that + // coordinates cross-process writers / readers (see roadmap/v5/MULTI_PROCESS_WAL.md). + // The SHM file lives next to the WAL so both have the same lifetime. + if (config.AllowMultiProcessAccess) + { + var shmPath = walPath + "-shm"; + _shm = Transactions.WalSharedMemory.Open(shmPath, config.PageSize); + _walOffsets = new ConcurrentDictionary(); + + // If a previous writer crashed without releasing the writer lock, the + // recorded PID will not be alive — clear it so this process (or another) + // can re-acquire the lock. The OS-level mutex / OFD lock has already been + // auto-released by the kernel; this only clears our PID stamp. + _shm.ForceClearStaleWriter(); + } - _wal = new WriteAheadLog(walPath, walCryptoProvider, config.LockTimeout.WriteTimeoutMs, config.AllowMultiProcessAccess); - _walCache = new ConcurrentDictionary>(); - _walIndex = new ConcurrentDictionary(); - _activeTransactions = new ConcurrentDictionary(); - _nextTransactionId = 0; // Interlocked.Increment pre-increments, so first txnId == 1. + // Admission gate: limits concurrent commit pressure on WAL/commit locks. + _writerGate = config.LockTimeout.MaxConcurrentWriters > 0 + ? new SemaphoreSlim(config.LockTimeout.MaxConcurrentWriters, config.LockTimeout.MaxConcurrentWriters) + : null; - // ── Multi-process WAL coordination (opt-in) ───────────────────────── - // When AllowMultiProcessAccess is true, open the .wal-shm sidecar that - // coordinates cross-process writers / readers (see roadmap/v5/MULTI_PROCESS_WAL.md). - // The SHM file lives next to the WAL so both have the same lifetime. - if (config.AllowMultiProcessAccess) - { - var shmPath = walPath + "-shm"; - _shm = Transactions.WalSharedMemory.Open(shmPath, config.PageSize); - _walOffsets = new ConcurrentDictionary(); - - // If a previous writer crashed without releasing the writer lock, the - // recorded PID will not be alive — clear it so this process (or another) - // can re-acquire the lock. The OS-level mutex / OFD lock has already been - // auto-released by the kernel; this only clears our PID stamp. - _shm.ForceClearStaleWriter(); + // Start the group commit writer. + _commitChannel = Channel.CreateBounded(new BoundedChannelOptions(4096) + { + FullMode = BoundedChannelFullMode.Wait, + SingleReader = true, + SingleWriter = false + }); + _writerTask = Task.Run(() => GroupCommitWriterAsync(_writerCts.Token)); + + // RecoverAsync from WAL if exists (crash recovery or resume after close) + // This replays any committed transactions not yet checkpointed + if (_wal.GetCurrentSize() > 0) + { + RecoverAsync().GetAwaiter().GetResult(); + } + + InitializeDictionary(); + InitializeKv(); + + // Create and start checkpoint manager + // _checkpointManager = new Transactions.CheckpointManager(this); + // _checkpointManager.StartAutoCheckpoint(); } - - // Admission gate: limits concurrent commit pressure on WAL/commit locks. - _writerGate = config.LockTimeout.MaxConcurrentWriters > 0 - ? new SemaphoreSlim(config.LockTimeout.MaxConcurrentWriters, config.LockTimeout.MaxConcurrentWriters) - : null; - - // Start the group commit writer. - _commitChannel = Channel.CreateBounded(new BoundedChannelOptions(4096) + catch { - FullMode = BoundedChannelFullMode.Wait, - SingleReader = true, - SingleWriter = false - }); - _writerTask = Task.Run(() => GroupCommitWriterAsync(_writerCts.Token)); - - // RecoverAsync from WAL if exists (crash recovery or resume after close) - // This replays any committed transactions not yet checkpointed - if (_wal.GetCurrentSize() > 0) - { - RecoverAsync().GetAwaiter().GetResult(); + try { _writerCts.Cancel(); } catch { /* best-effort */ } + try { _writerTask?.Wait(TimeSpan.FromSeconds(5)); } catch { /* best-effort */ } + try { _wal?.Dispose(); } catch { /* best-effort */ } + try { _pageFile?.Dispose(); } catch { /* best-effort */ } + try { _indexFile?.Dispose(); } catch { /* best-effort */ } + try { _writerGate?.Dispose(); } catch { /* best-effort */ } + try { _shm?.Dispose(); } catch { /* best-effort */ } + if (walCryptoProvider is IDisposable disposableWalCrypto) + try { disposableWalCrypto.Dispose(); } catch { /* best-effort */ } + try { _writerCts.Dispose(); } catch { /* best-effort */ } + throw; } - - InitializeDictionary(); - InitializeKv(); - - // Create and start checkpoint manager - // _checkpointManager = new Transactions.CheckpointManager(this); - // _checkpointManager.StartAutoCheckpoint(); } /// @@ -315,9 +334,9 @@ public void Dispose() } // 3. Close WAL and PageFile. - _wal?.Dispose(); - _pageFile?.Dispose(); - _indexFile?.Dispose(); + try { _wal?.Dispose(); } catch { /* best-effort */ } + try { _pageFile?.Dispose(); } catch { /* best-effort */ } + try { _indexFile?.Dispose(); } catch { /* best-effort */ } // 4. Close per-collection PageFiles (Phase 4) if (_collectionFiles != null) @@ -329,11 +348,11 @@ public void Dispose() _collectionFiles.Clear(); } - _commitLock?.Dispose(); - _metadataLock?.Dispose(); - _writerGate?.Dispose(); - _metrics?.Dispose(); - _shm?.Dispose(); + try { _commitLock?.Dispose(); } catch { /* best-effort */ } + try { _metadataLock?.Dispose(); } catch { /* best-effort */ } + try { _writerGate?.Dispose(); } catch { /* best-effort */ } + try { _metrics?.Dispose(); } catch { /* best-effort */ } + try { _shm?.Dispose(); } catch { /* best-effort */ } } internal void RegisterCdc(CDC.ChangeStreamDispatcher cdc)