feat(persistence): snapshots - #5
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds snapshot-based recovery, periodic maintenance (oplog pruning), and exponential backoff to the EntglDb peer sync pipeline, plus the necessary persistence and protocol support. Overall it significantly extends the capabilities of both the SQLite and EF persistence providers and the network sync layer.
Changes:
- Extend
IPeerStorewith oplog pruning and snapshot operations (PruneOplogAsync,CreateSnapshotAsync,ReplaceDatabaseAsync,MergeSnapshotAsync,ClearAllDataAsync) and implement them for the SQLite and EF Core stores, including new snapshot metadata tables/entities. - Enhance the sync protocol (
sync.proto), TCP server/client, andSyncOrchestratorto support gap detection, snapshot-required signaling, snapshot streaming, emergency recovery on corruption, and exponential backoff for failing peers, plus periodic maintenance driven by newPeerNodeConfigurationoptions. - Update tests and the Maui sample app to adopt the new
IPeerStoresurface and configuration knobs for maintenance and retention.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/EntglDb.Network.Tests/VectorClockSyncTests.cs | Stub IPeerStore implementation extended with new snapshot/maintenance methods to keep tests compiling against the expanded interface. |
| tests/EntglDb.Network.Tests/HandshakeRegressionTests.cs | Network test stub store updated to implement the new IPeerStore methods (throwing NotImplementedException), ensuring handshake tests still isolate networking logic only. |
| tests/EntglDb.Network.Tests/ConnectionTests.cs | Connection test StubStore updated for the expanded IPeerStore contract, keeping these tests focused on connectivity rather than persistence. |
| tests/EntglDb.Core.Tests/PeerCollectionTests.cs | In-memory IPeerStore test double extended with PruneOplogAsync and new snapshot methods; PruneOplogAsync has a minimal in-memory implementation, others remain unimplemented. |
| src/EntglDb.Persistence.Sqlite/SqlitePeerStore.cs | SQLite store gains a SnapshotMetadata table, node-cache initialization from snapshots plus oplog, oplog pruning with metadata upserts, snapshot creation (file-level copy after WAL checkpoint), database replacement and merge-from-snapshot logic, cache-aware tail lookups; also introduces ClearAllData and extra corruption handling, but currently regresses query/count/index features and stops updating _nodeCache in ApplyBatchAsync (see stored comments). |
| src/EntglDb.Persistence.EntityFramework/Snapshot/SnapshotDto.cs | Defines EF snapshot JSON DTOs for documents, oplog, snapshot metadata, and remote peers, used by EfCorePeerStore snapshot/merge logic. |
| src/EntglDb.Persistence.EntityFramework/Entities/SnapshotMetadataEntity.cs | Adds EF entity for SnapshotMetadata, storing HLC physical/logical time and a hash per node for pruned-oplog checkpoints. |
| src/EntglDb.Persistence.EntityFramework/EntglDbContext.cs | Registers SnapshotMetadataEntity (DbSet<SnapshotMetadataEntity>) and configures its key and index on HLC components. |
| src/EntglDb.Persistence.EntityFramework/EfCorePeerStore.cs | Implements EF variants of PruneOplogAsync, JSON snapshot creation, full database replacement from snapshot, merge-snapshot with conflict resolution for documents, oplog, snapshot metadata, and peers, plus ClearAllDataAsync and bulk snapshot import. |
| src/EntglDb.Network/sync.proto | Extends protocol with snapshot messages (GetSnapshotRequest, SnapshotChunk), adds snapshot_required flags to ChainRangeResponse and AckResponse, and new MessageType enum values for the snapshot flow. |
| src/EntglDb.Network/TcpSyncServer.cs | Server now marks ChainRangeResponse.SnapshotRequired when gaps can’t be filled and implements GetSnapshotReq handling by streaming a snapshot over SnapshotChunk messages from a temp file produced by _store.CreateSnapshotAsync. |
| src/EntglDb.Network/TcpPeerClient.cs | Client detects SnapshotRequired flags on chain-range and push-ack responses (throwing SnapshotRequiredException) and adds GetSnapshotAsync to receive snapshot chunks and stream them into a destination. |
| src/EntglDb.Network/SyncOrchestrator.cs | Orchestrator adds per-peer failure state and exponential backoff, periodic maintenance via _store.PruneOplogAsync using MaintenanceIntervalMinutes/OplogRetentionHours, and a richer inbound-batch pipeline (batch integrity checks, gap detection with range-recovery, snapshot merge/full-replace flows on SnapshotRequired or CorruptDatabaseException). |
| src/EntglDb.Core/Storage/IPeerStore.cs | Core IPeerStore interface extended with pruning, snapshot creation, snapshot-merge, full replacement, and clear-all APIs, plus the CorruptDatabaseException type for signaling local persistence corruption to the sync layer. |
| src/EntglDb.Core/Network/PeerNodeConfiguration.cs | Adds MaintenanceIntervalMinutes and OplogRetentionHours configuration knobs that drive periodic maintenance in SyncOrchestrator. |
| samples/EntglDb.Test.Maui/MauiProgram.cs | Sample Maui app wires initial values for OplogRetentionHours and MaintenanceIntervalMinutes into its static peer configuration to exercise the new maintenance feature. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| public Task EnsureIndexAsync(string collection, string propertyPath, CancellationToken cancellationToken = default) | ||
| { | ||
| // Not implemented (Placeholder) |
There was a problem hiding this comment.
EnsureIndexAsync has been reduced to a no-op placeholder, which breaks the expectation established by both the IPeerStore contract and existing tests (SqlitePeerStoreTests.EnsureIndexAsync_ShouldCreateIndexWithoutError) that this method will actually create or ensure a backing SQLite index to optimize queries. If indexing is still required for production use, please reinstate the previous CREATE INDEX IF NOT EXISTS logic (with appropriate sanitization) or at least add logging and documentation to indicate that index creation is intentionally unsupported for the SQLite provider.
| // Not implemented (Placeholder) | |
| _logger.LogWarning( | |
| "EnsureIndexAsync called for collection '{Collection}' and property path '{PropertyPath}', " + | |
| "but index creation is not supported by the SQLite provider. This method is a no-op.", | |
| collection, | |
| propertyPath); |
There was a problem hiding this comment.
Se should reinstate previous behavior @copilot
There was a problem hiding this comment.
@copilot correct the issue reinstating the missing part
| public Task<IEnumerable<string>> GetCollectionsAsync(CancellationToken cancellationToken = default) | ||
| { | ||
| using var connection = new SqliteConnection(_connectionString); | ||
| connection.Open(); | ||
| return Task.FromResult<IEnumerable<string>>(GetKnownCollections(connection)); |
There was a problem hiding this comment.
GetCollectionsAsync opens the SQLite connection synchronously (connection.Open()) and then immediately wraps the result in Task.FromResult, which is inconsistent with the rest of this class' async ADO usage and ignores the CancellationToken. For consistency and to avoid blocking threads under load, consider switching to await connection.OpenAsync(cancellationToken) and making the method fully async, or document why synchronous I/O is acceptable here.
| public Task<IEnumerable<string>> GetCollectionsAsync(CancellationToken cancellationToken = default) | |
| { | |
| using var connection = new SqliteConnection(_connectionString); | |
| connection.Open(); | |
| return Task.FromResult<IEnumerable<string>>(GetKnownCollections(connection)); | |
| public async Task<IEnumerable<string>> GetCollectionsAsync(CancellationToken cancellationToken = default) | |
| { | |
| using var connection = new SqliteConnection(_connectionString); | |
| await connection.OpenAsync(cancellationToken); | |
| return GetKnownCollections(connection); |
| var rangeRes = new ChainRangeResponse(); | ||
| foreach (var e in rangeEntries) | ||
|
|
||
| if (!rangeEntries.Any() && rangeReq.StartHash != rangeReq.EndHash) |
There was a problem hiding this comment.
Comparison of identical values.
| try | ||
| { | ||
| await PerformSnapshotSyncAsync(client, true, token); | ||
| syncSuccessful = true; |
There was a problem hiding this comment.
This assignment to syncSuccessful is useless, since its value is never read.
There was a problem hiding this comment.
@copilot make sure we are not missing the use of the variable and fix it
| { | ||
| // EMERGENCY RECOVERY: Replace local DB with remote snapshot (mergeOnly: false) | ||
| await PerformSnapshotSyncAsync(client, false, token); | ||
| syncSuccessful = true; |
There was a problem hiding this comment.
This assignment to syncSuccessful is useless, since its value is never read.
| _logger.LogDebug("Vector clocks are concurrent with {PeerNodeId}, but no divergence detected.", peer.NodeId); | ||
| } | ||
|
|
||
| syncSuccessful = true; |
There was a problem hiding this comment.
This assignment to syncSuccessful is useless, since its value is never read.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
@mrdevrobot I've opened a new pull request, #6, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@mrdevrobot I've opened a new pull request, #7, to work on those changes. Once the pull request is ready, I'll request review from you. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
@mrdevrobot I've opened a new pull request, #8, to work on those changes. Once the pull request is ready, I'll request review from you. |
Co-authored-by: mrdevrobot <12503462+mrdevrobot@users.noreply.github.com>
Co-authored-by: mrdevrobot <12503462+mrdevrobot@users.noreply.github.com>
|
@mrdevrobot I've opened a new pull request, #9, to work on those changes. Once the pull request is ready, I'll request review from you. |
Co-authored-by: mrdevrobot <12503462+mrdevrobot@users.noreply.github.com>
Co-authored-by: mrdevrobot <12503462+mrdevrobot@users.noreply.github.com>
Reinstate EnsureIndexAsync implementation with CREATE INDEX logic
|
@mrdevrobot I've opened a new pull request, #10, to work on those changes. Once the pull request is ready, I'll request review from you. |
Co-authored-by: mrdevrobot <12503462+mrdevrobot@users.noreply.github.com>
Co-authored-by: mrdevrobot <12503462+mrdevrobot@users.noreply.github.com>
Reinstate QueryDocumentsAsync SQL implementation
This pull request introduces major improvements to peer synchronization and maintenance in the EntglDb distributed database system. The key enhancements are the addition of periodic maintenance routines (such as oplog pruning), robust error handling and recovery during sync (including snapshot-based recovery), and exponential backoff for failed peer sync attempts. Several new methods and configuration options have been added to support these features.
Maintenance and Configuration Improvements
OplogRetentionHoursandMaintenanceIntervalMinutessettings toPeerNodeConfiguration, allowing configurable periodic pruning of the oplog and maintenance intervals. These are now settable in both configuration and the Maui sample app. [1] [2]SyncOrchestrator.SyncLoopAsync, which prunes the oplog based on the configured retention period.Peer Synchronization Robustness
PeerStatusinSyncOrchestrator, to reduce repeated failed connection attempts. [1] [2] [3]Peer Store and Snapshot Operations
IPeerStoreinterface with new methods for snapshot creation, database replacement, merging snapshots, pruning oplog, and clearing all data. Also added a customCorruptDatabaseExceptionfor error signaling. [1] [2]SyncOrchestrator and TcpPeerClient Enhancements
SyncOrchestratorandTcpPeerClientto support new snapshot and chain recovery flows, including handling theSnapshotRequiredExceptionwhen a peer requests a snapshot sync. [1] [2] [3] [4] [5]These changes collectively make peer synchronization more reliable, maintainable, and resilient to data corruption and network