fix(#4333): persist migratedFileIds in schema.json to survive restart#4342
Conversation
There was a problem hiding this comment.
Code Review
This pull request addresses issue #4333 by persisting the migratedFileIds map in schema.json to prevent WAL recovery from silently skipping pages of missing files after a restart. It serializes and deserializes the map in LocalSchema and updates TransactionManager.applyChanges() to check this map, logging a warning for genuinely unknown missing files. Feedback points out that setMigratedFileId needs to call saveConfiguration() to ensure the updated map is actually written to disk. Additionally, checking for null values when deserializing migratedFileIds is recommended to avoid potential JSONExceptions, and the warning log level for missing files should be reconsidered to avoid noise during normal Raft replay or crash recovery.
| if (!migratedFileIds.isEmpty()) { | ||
| final JSONObject migratedJSON = new JSONObject(); | ||
| for (final Map.Entry<Integer, Integer> entry : migratedFileIds.entrySet()) | ||
| migratedJSON.put(String.valueOf(entry.getKey()), entry.getValue()); | ||
| root.put("migratedFileIds", migratedJSON); | ||
| } |
There was a problem hiding this comment.
While migratedFileIds is now correctly serialized and deserialized, setMigratedFileId(int, int) does not call saveConfiguration() or mark the schema as dirty.
If a compaction occurs and updates the migration map, but no other schema changes happen before the database is closed, the schema will not be considered dirty (dirtyGeneration.get() > savedGeneration will be false) and close() will not persist the updated map to schema.json.
To ensure the migration map is always persisted, setMigratedFileId should call saveConfiguration() (which will either save immediately or postpone it by marking the schema dirty if a transaction is active):
public void setMigratedFileId(final int oldFileId, final int newFileId) {
LogManager.instance().log(this, Level.FINE, "Migrating file id %d to %d", null, oldFileId, newFileId);
migratedFileIds.put(oldFileId, newFileId);
saveConfiguration();
}| if (root.has("migratedFileIds")) { | ||
| final JSONObject migratedJSON = root.getJSONObject("migratedFileIds"); | ||
| for (final String key : migratedJSON.keySet()) | ||
| migratedFileIds.put(Integer.parseInt(key), migratedJSON.getInt(key)); | ||
| } |
There was a problem hiding this comment.
If migratedFileIds is present in the JSON but has a null value (e.g., "migratedFileIds": null), root.getJSONObject("migratedFileIds") will throw a JSONException. Using !root.isNull(...) is safer and prevents potential crashes during schema loading.
| if (root.has("migratedFileIds")) { | |
| final JSONObject migratedJSON = root.getJSONObject("migratedFileIds"); | |
| for (final String key : migratedJSON.keySet()) | |
| migratedFileIds.put(Integer.parseInt(key), migratedJSON.getInt(key)); | |
| } | |
| if (root.has("migratedFileIds") && !root.isNull("migratedFileIds")) { | |
| final JSONObject migratedJSON = root.getJSONObject("migratedFileIds"); | |
| for (final String key : migratedJSON.keySet()) | |
| migratedFileIds.put(Integer.parseInt(key), migratedJSON.getInt(key)); | |
| } |
| else | ||
| LogManager.instance().log(this, Level.WARNING, | ||
| "Skipping page for missing file %d during transaction apply, file was deleted or migration map is incomplete", null, | ||
| txPage.fileId); |
There was a problem hiding this comment.
Logging a WARNING for any missing file not present in the migration map might be too noisy. During normal Raft replay or crash recovery, files deleted intentionally by schema changes (e.g., DROP TYPE or DROP INDEX) will also trigger this warning, potentially flooding the logs with false positives. Consider if INFO or keeping FINE is more appropriate, or if there is a way to track intentionally deleted files.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 13 |
🟢 Coverage 64.71% diff coverage
Metric Results Coverage variation Report missing for a68f6631 Diff coverage ✅ 64.71% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (a68f663) Report Missing Report Missing Report Missing Head commit (2789ec0) 155481 103252 66.41% Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#4342) 17 11 64.71% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%1 Codacy didn't receive coverage data for the commit, or there was an error processing the received data. Check your integration for errors and validate that your coverage setup is correct.
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
|
test comment please ignore |
Code Review - fix(#4333): persist migratedFileIds in schema.jsonThe fix is correct in its core approach: serializing Bug: double
|
| Severity | Item |
|---|---|
| Medium | Double saveConfiguration() call - redundant disk write per compaction |
| Medium | migratedFileIdsRoundTrip - compaction success not verified, test may give misleading failures |
| Medium | docs/4333-persist-migrated-file-ids.md - tracking document should not be in the repo |
| Low | Map grows without bound (pre-existing, but persistence makes it permanent) |
| Nit | INFO log wording slightly alarming |
The core logic (serialize/deserialize migratedFileIds, consult map in applyChanges) is sound. The double-save and the docs file are the most important things to fix before merge.
Code ReviewSummary: This PR correctly identifies and fixes a real bug - Bug: WARNING log for intentional DROP TYPE / DROP INDEXThe original comment in
That case (DROP TYPE / DROP INDEX) is still completely valid and now logs at The intent of the new WARNING is good (flag truly unexpected missing files), but currently it fires on both unexpected files and intentional schema-drop files. Consider whether a separate mechanism - such as recording intentionally dropped file IDs in the schema JSON as a Unbounded map growth in
|
Code Review - fix(#4333): persist migratedFileIds in schema.json to survive restartOverviewThis PR fixes a real correctness bug: Issues1. Double Both callers of
Now that 2. There is no eviction or pruning of entries from 3. This file contains internal review-cycle notes ("gemini-code-assist reviewed (COMMENTED)", "State: timeout", links back to the PR being merged). It is meta-documentation about the review process, not documentation about the feature or schema format. It will become stale immediately after merge. PR descriptions and issue comments are the right place for this content. Minor Points4. Second test does not assert on In 5. The two tests are nearly identical Both tests: create a schema, insert 1000 records, compact, close, reopen, and assert the migration entry survived. The only difference is the entity name. Consider merging into a single test or giving the second a distinct scenario (e.g., multiple sequential compactions, or a migration followed by a type drop). What Works Well
|
LocalSchema.migratedFileIds (old->new fileId map from LSM compaction) was a heap-only ConcurrentHashMap omitted from toJSON()/schema.json. After a restart the map was always empty, so TransactionManager.applyChanges could not distinguish a safe compaction-skip from a genuine missing-file data loss and silently dropped WAL pages at FINE level. Changes: - LocalSchema.toJSON(): serialize migratedFileIds as JSON object under "migratedFileIds" key (only emitted when non-empty) - LocalSchema loading: restore migratedFileIds from the JSON block on open - TransactionManager.applyChanges: consult the migration map when a fileId is missing; log FINE for known migrations, WARNING for unknown ones
…evel
- setMigratedFileId() now calls saveConfiguration() so the migration map
is persisted even when no other schema change follows compaction
- loading: add !root.isNull("migratedFileIds") guard before getJSONObject()
to avoid JSONException if the field is explicitly null
- TransactionManager: downgrade unknown-missing-file log from WARNING to INFO
to avoid noise during intentional schema-drop file deletions in Raft replay
014a198 to
2789ec0
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4342 +/- ##
==========================================
- Coverage 65.37% 64.96% -0.42%
==========================================
Files 1610 1610
Lines 123789 123803 +14
Branches 26776 26781 +5
==========================================
- Hits 80925 80426 -499
- Misses 31429 32048 +619
+ Partials 11435 11329 -106 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Closes #4333
Summary
LocalSchema.migratedFileIds(the old->new fileId map written by LSM/vector index compaction) was a heap-onlyConcurrentHashMapthat was never included inschema.json. After a restart the map was always empty, soTransactionManager.applyChangescould not distinguish a safe compaction-migration skip from an unknown missing-file condition, silently dropping WAL pages at FINE level after every compaction+restart.Changes:
LocalSchema.toJSON(): serializemigratedFileIdsas a JSON object under the key"migratedFileIds"(only emitted when non-empty, so existing schema files without the key remain compatible)LocalSchemaloading: restoremigratedFileIdsfrom the JSON block on open, clearing the map first for idempotencyTransactionManager.applyChanges(): consult the migration map when a fileId is missing; log FINE for known compaction-migrations (safe skip, data already in the new file), WARNING for unknown missing files (deleted by schema change or incomplete migration map)Test plan
MigratedFileIdsPersistenceTest#migratedFileIdsArePersisted- after compaction triggerssetMigratedFileId, close and reopen the database; migration map must still be populatedMigratedFileIdsPersistenceTest#migratedFileIdsRoundTrip- round-trip the map through close+reopen and verify exact old->new entriesLSMTreeIndexCompactionTest- all existing compaction tests passLockFilesInOrderFileMigrationTest- existing file-migration regression test passesConcurrentCompactionTest- concurrent compaction tests pass