Keep the docid lookup (.spt) in sync with the index format version on ALTER; repair v70 lookups under a v71 header (#4852) - #4858
Conversation
|
CI follow-up, pushed in the second commit:
|
klirichek
left a comment
There was a problem hiding this comment.
Ideally it would be perfect if .spt format was constructed as backward-compatible; but right now we already missed this train.
Well, if it is happened, that current builds break an index with ALTER, that is should be fixed as a root case.
That is, if we going to rewrite header, the spt should also be rewritten, if necessary, at the same time, so that situation when header has v.71+, but .spt lookup doesn't match it should be impossible. So, if we rewrite header, we should check, if we rewrite from the pre-UUID state, and if necessary, rewrite .sph also. Usually 'alter' operations are expected as long-running, so one more action there is perfectly ok. So, daemons responsibility is this 'writing in sync'.
With bad versions of daemon, if table is never altered, it has old header and old lookup, and nobody cares.
But if it's header came out of sync because of alter, it is better just to reject load such table with clear message why. Universal advice to a user in this case - 'check the table with indextool'.
Future versions of daemon should not try to fix anything 'on the fly' in case table is already inconsistent. That is puprpose of indextool.
|
|
||
| sphWarning ( "%s: docid lookup is in the pre-v.%u layout while the header is v.%u; upgrading the lookup in place", sFile.cstr(), DOCID_LOOKUP_UUID_VERSION, m_uVersion ); | ||
| m_tDocidLookup.Reset(); | ||
| if ( !UpgradeDocidLookupFile ( sFile, uActual, m_sLastError ) || !m_tDocidLookup.Setup ( sFile, m_sLastError, false ) ) |
There was a problem hiding this comment.
not sure at all that file should be rewritten during simple prealloc().
It looks more safe in this case just to report the error and reject loading table.
There was a problem hiding this comment.
Agreed - removed in 6b83674. Prealloc only validates now and refuses the table: docid lookup layout does not match index format v.72: ... ; check the table with indextool, then rebuild or restore it (manticoresearch#4852). Verified on a chunk with a v.72 header over a v.70-layout lookup: the daemon starts, the table is refused with that message, and indextool --check reports the same.
| return false; | ||
|
|
||
| // the new header carries the current format version; the data files must match it | ||
| if ( !const_cast<CSphIndex_VLN*>(this)->UpgradeDocidLookup ( sError ) ) |
There was a problem hiding this comment.
That is bad idea. RewriteHeader should perform exactly one thing - rewrite the header. I.e. only .sph file; with most safe way. Break the const contract and perform any other changes is too legacy; it will be really hard to diagnose/support it in future.
There was a problem hiding this comment.
Agreed - RewriteHeader() only rewrites the header again (6b83674). Its one caller, the ATTACH ext-copy (which ALTER ... RENAME goes through via CREATE TABLE LIKE), upgrades the docid lookup first through a CSphIndex virtual, in the same operation - so 'writing in sync' stays with the operation, and the header writer keeps its const contract.
| return true; | ||
| } | ||
|
|
||
| static int64_t GetDocidLookupHeaderSize ( DWORD uIndexVersion ) |
There was a problem hiding this comment.
what if on further upgrade some new field will be added?
If addressed only one pivot version, say, 71, it should not be sensible if in v.81 header size again changed.
Also, if it is sticked to concrete version - that is totally ok and desirable to name the function more concretely, as generic name is a bit confusing.
There was a problem hiding this comment.
Reworked in 6b83674: the sizes are now explicit per-version constants (DOCID_LOOKUP_HEADER_SIZE_V65 / _V71) behind DocidLookupHeaderSize(), and a static_assert on INDEX_FORMAT_VERSION next to the upgrade code makes the next format bump fail to compile until the layout table is re-confirmed - a v.81 header change cannot silently pass or fail this validation any more.
… ALTER; repair v70 lookups under a v71 header (manticoresoftware#4852) Fixes manticoresoftware#4852. Index format v.71 added an 8-byte UUID-entries offset to the .spt header, and LookupReader_c is gated on the chunk's index_format_version. In-place operations that rewrite a disk chunk header (ALTER TABLE ADD/DROP COLUMN, ADD/DROP field, SaveHeader, RewriteHeader) stamp the current INDEX_FORMAT_VERSION but leave .spt untouched. On a chunk built before v.71 the lookup is then decoded 8 bytes off: every docid->rowid lookup returns garbage rowids, and whatever consumes them segfaults - UPDATE (Update_CollectRowPtrs -> Update_Blobs), DELETE/REPLACE (KillMulti), id filters and docstore reads (GetRowidByDocid). In memory the chunk keeps the version it was loaded with, so nothing fails until the daemon restarts; then binlog replay of the same UPDATE crash-loops the startup. Reproduced by replaying the reporter's poisoned binlog against the affected chunk under gdb: header 71, .spt in the v.70 layout (first checkpoint at offset 471 instead of 24616), rowid 0xf5010000 for docid 191429. - docidlookup: CheckDocidLookupFormat() validates the layout against a format version (the first checkpoint must start right after the header and the checkpoint table), DetectDocidLookupVersion() names the layout actually present, UpgradeDocidLookupFile() rewrites a pre-v.71 file into the current layout via a temporary file. - CSphIndex_VLN::UpgradeDocidLookup() runs before the header is rewritten in AddRemoveAttribute, AddRemoveField, SaveHeader and RewriteHeader, so a chunk is brought to the current format together with its header. - PreallocDocidLookup() validates the lookup on load. The unambiguous case "v.71+ header over a v.70 lookup" (what older daemons produced on ALTER) is repaired in place with a warning; any other mismatch fails the chunk with a clear error instead of crashing later. - indextool --check reports the mismatch. - gtest UuidDocidLookupTest.FormatCheckAndUpgrade. Verified on the reporter's data (29.0.2 chunks): the corrupted chunk is repaired on load and the previously crashing UPDATE replays; on a pre-v.71 copy ALTER TABLE ADD COLUMN upgrades the lookup and lookups keep working across a restart; indextool --check passes afterwards.
… layouts predate the 2022 split-lookup header and were never validated; test_250 rotates a v.54 table with a 10-byte .spt), make the unrecoverable-mismatch error actionable (manticoresoftware#4852)
…written in sync by the operations themselves - PreallocDocidLookup() only validates: an inconsistent lookup refuses the table with a clear error pointing at indextool; the in-place repair is gone. - RewriteHeader() is pure again (only the header); the one operation that calls it (ATTACH ext-copy, which ALTER ... RENAME uses through CREATE LIKE) upgrades the docid lookup first via a CSphIndex virtual, in the same operation - the header and the version-gated files are written in sync. - The dead SaveHeader() hook is dropped. - The header-size helper is named concretely (DOCID_LOOKUP_HEADER_SIZE_V65 / _V71 constants) and a static_assert on INDEX_FORMAT_VERSION forces a review of the layout table on every future format bump.
8e393bc to
6b83674
Compare
|
Thanks for the direction, @klirichek - pushed 6b83674 (the branch is also rebased onto current main):
Verified: the docidlookup gtests pass; a freshly built table whose chunk lookup was rewritten into the v.70 layout under its v.72 header is refused on load with the message above and flagged by |
klirichek
left a comment
There was a problem hiding this comment.
I've think about how possible is to fix it globally with version upgrade, may be you can suggest something better, or use my solution.
In addition, we report that index should be fixed, but provide no instrument for it. May be add option to indextool, or even to index-converter which will just do these things offline. Full rebuilding of the index will also work, but in the case we can rewrite the file easy way, it is more appropriate.
|
|
||
| // a format version bump must confirm that the .spt layout knowledge in docidlookup.cpp | ||
| // (DocidLookupHeaderSize and the DOCID_LOOKUP_* constants) still holds, then move this tripwire | ||
| static_assert ( INDEX_FORMAT_VERSION==72, "INDEX_FORMAT_VERSION changed: verify the .spt header layout table in docidlookup.cpp, then update this assert" ); |
There was a problem hiding this comment.
read about upgrading version to 73-74. It should help to totally avoid that static assert. We don't need to support this fix during a long time; let's fix once and avoid future checks
There was a problem hiding this comment.
Gone in 84a12d0 - with v.73+ never checked, the assert has no reason to exist.
… offline fix in indextool - INDEX_FORMAT_VERSION 72 -> 74. v.71/v.72 headers are "suspect" (their daemons could rewrite a header on ALTER without converting the .spt), so only they get the lookup layout check on load; v.73/v.74 are the fixed twins (73 = the v.71 feature set, 74 = v.72's), written exclusively with the lookup in sync and never checked - the static_assert tripwire is gone. - A rewritten header of an existing table stamps the fixed version (71->73, 72->74) via BuildHeader_t::m_uFormatVersion; fresh builds keep stamping the current version. - indextool --fix-docid-lookup <table>: offline repair for a rejected table - converts a pre-v.71 lookup to the current layout when needed and stamps the fixed header version, per disk chunk for RT tables; a consistent suspect table just gets the stamp. - The ATTACH ext-copy path uses one pointer for upgrade + rewrite.
|
Pushed 84a12d0 with your scheme, @klirichek:
Verified with a build of this branch: a fresh table stamps v.74; a fabricated v.72 header over a v.70-layout lookup is refused on load, |
klirichek
left a comment
There was a problem hiding this comment.
I approve in general, imply that tests should pass before merge.
|
Thanks for the approval, @klirichek! CI is fully green on 84a12d0 — 49 checks passed, no failures. |
Fixes #4852.
Root cause
Index format v.71 (013bfbb, "harden and optimize UUID primary IDs") added an 8-byte UUID-entries offset to the
.spt(docid lookup) header, andLookupReader_c::SetData()is gated on the chunk'sindex_format_version. Every in-place operation that rewrites a disk chunk header —ALTER TABLE ... ADD/DROP COLUMN,ADD/DROPfield,SaveHeader(),RewriteHeader()— stamps the currentINDEX_FORMAT_VERSIONbut leaves.sptuntouched.So on a chunk built before v.71 and ALTERed by a v.71+ daemon, the lookup is decoded 8 bytes off: every docid→rowid lookup returns garbage rowids, and whatever consumes them segfaults — UPDATE (
Update_CollectRowPtrs→Update_Blobs), DELETE/REPLACE (KillMulti), id filters and docstore reads (GetRowidByDocid). In memory the chunk keeps the version it was loaded with, so nothing fails until the daemon restarts; then the binlog replay of the same UPDATE crash-loops the startup — exactly the sequence in the issue (our chunks were built by 28.4.4 = v.70 and ALTERed after upgrading to 29.0.2; the log shows a clean SIGTERM at 11:29 and every crash after).Reproduced by replaying the poisoned binlog against the affected chunk under gdb: header
71,.sptin the v.70 layout (first checkpoint at offset 471 instead of 24616),Intersect()yields rowid0xf5010000for docid 191429 →DeadRowMap_c::IsSetonmain/sphGetBlobAttron 29.0.2. The retained data dir has 120 chunks in that state.Change
docidlookup.cpp/h:CheckDocidLookupFormat()validates the layout against a format version (the first checkpoint must start right after the header and the checkpoint table);DetectDocidLookupVersion()names the layout actually present;UpgradeDocidLookupFile()rewrites a pre-v.71 file into the current layout via a temporary file (header field inserted, checkpoint offsets shifted).CSphIndex_VLN::UpgradeDocidLookup()runs before the header is rewritten inAddRemoveAttribute,AddRemoveField,SaveHeaderandRewriteHeader, so a chunk is brought to the current format together with its header (andm_uVersionis bumped only then).PreallocDocidLookup()validates the lookup on load. The unambiguous case — v.71+ header over a v.70 lookup, i.e. what released daemons produced on ALTER — is repaired in place with a warning; any other mismatch fails the chunk with a clear error instead of crashing later.indextool --checkreports the mismatch.UuidDocidLookupTest.FormatCheckAndUpgrade(v.70 layout detected under a v.71 version, upgrade, round-trip).Not addressed here (separate issue candidate):
RtIndex_c::AddRemoveAttributetreats a failing per-chunkAddRemoveAttributeas a warning while the RT schema has already advanced (fixme: we can't rollback).Verification
On the reporter's actual data (29.0.2 chunks,
planetrc_1_en, 197k docs):docid lookup is in the pre-v.71 layout while the header is v.71; upgrading the lookup in place, the table loads, the previously crash-looping UPDATE replays, UPDATE/DELETE/WHERE id INon the affected docs work,indextool --checkpasses afterwards;ALTER TABLE ... ADD COLUMNupgrades the lookup, headers go to the current version, lookups keep working across a restart;indextool --checkon the corrupted chunk:FAILED, docid lookup layout does not match index format v.71: first checkpoint at 471, expected 24616 (the lookup is in the pre-v.71 layout ...).A small scanner that finds affected chunks in a data dir (header version vs
.sptlayout) is attached to the issue.