Skip to content

Commit 69b3e44

Browse files
committed
Report leftover MVV mark bits in IntegrityCheck
A prune interrupted before the issue #286 fix could leave 0x8000 mark bits behind in on-disk version length fields. Since PR #288 the read paths strip the mark bit silently, so such versions read normally and icheck reported affected volumes as clean. Add MVV.countMarkedVersions and scan every data record during icheck: marked versions are counted in a new MarkedVersions counter (report, CSV and getMarkedVersionCount) and reported as a fault so affected volumes can be identified. icheck -p remains the remediation: pruning rewrites the marked versions. Fixes #290
1 parent b669a2f commit 69b3e44

4 files changed

Lines changed: 173 additions & 6 deletions

File tree

persistit/core/src/main/java/com/persistit/IntegrityCheck.java

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ private static class Counters {
9898
private long _mvvCount = 0;
9999
private long _mvvOverhead = 0;
100100
private long _mvvAntiValues = 0;
101+
private long _markedVersionCount = 0;
101102
private long _pruningErrorCount = 0;
102103
private long _prunedPageCount = 0;
103104
private long _garbagePageCount = 0;
@@ -118,6 +119,7 @@ private static class Counters {
118119
_mvvCount = counters._mvvCount;
119120
_mvvOverhead = counters._mvvOverhead;
120121
_mvvAntiValues = counters._mvvAntiValues;
122+
_markedVersionCount = counters._markedVersionCount;
121123
_pruningErrorCount = counters._pruningErrorCount;
122124
_prunedPageCount = counters._prunedPageCount;
123125
_garbagePageCount = counters._garbagePageCount;
@@ -135,6 +137,7 @@ void difference(final Counters counters) {
135137
_mvvCount = counters._mvvCount - _mvvCount;
136138
_mvvOverhead = counters._mvvOverhead - _mvvOverhead;
137139
_mvvAntiValues = counters._mvvAntiValues - _mvvAntiValues;
140+
_markedVersionCount = counters._markedVersionCount - _markedVersionCount;
138141
_pruningErrorCount = counters._pruningErrorCount - _pruningErrorCount;
139142
_prunedPageCount = counters._prunedPageCount - _prunedPageCount;
140143
_garbagePageCount = counters._garbagePageCount - _garbagePageCount;
@@ -144,20 +147,21 @@ void difference(final Counters counters) {
144147
public String toString() {
145148
return String.format("Index pages/bytes: %,d / %,d Data pages/bytes: %,d / %,d"
146149
+ " LongRec pages/bytes: %,d / %,d MVV pages/records/bytes/antivalues: "
147-
+ "%,d / %,d / %,d / %,d Holes %,d Pages pruned %,d", _indexPageCount, _indexBytesInUse,
148-
_dataPageCount, _dataBytesInUse, _longRecordPageCount, _longRecordBytesInUse, _mvvPageCount,
149-
_mvvCount, _mvvOverhead, _mvvAntiValues, _indexHoleCount, _prunedPageCount);
150+
+ "%,d / %,d / %,d / %,d Holes %,d Pages pruned %,d Marked versions %,d", _indexPageCount,
151+
_indexBytesInUse, _dataPageCount, _dataBytesInUse, _longRecordPageCount, _longRecordBytesInUse,
152+
_mvvPageCount, _mvvCount, _mvvOverhead, _mvvAntiValues, _indexHoleCount, _prunedPageCount,
153+
_markedVersionCount);
150154
}
151155

152156
private String toCSV() {
153-
return String.format("%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d", _indexPageCount, _indexBytesInUse,
157+
return String.format("%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d", _indexPageCount, _indexBytesInUse,
154158
_dataPageCount, _dataBytesInUse, _longRecordPageCount, _longRecordBytesInUse, _mvvPageCount,
155-
_mvvCount, _mvvOverhead, _mvvAntiValues, _indexHoleCount, _prunedPageCount);
159+
_mvvCount, _mvvOverhead, _mvvAntiValues, _indexHoleCount, _prunedPageCount, _markedVersionCount);
156160
}
157161

158162
private final static String CSV_HEADERS = "IndexPages,IndexBytes,"
159163
+ "DataPages,DataBytes,LongRecordPages,LongRecordBytes,MvvPages,"
160-
+ "MvvRecords,MvvOverhead,MvvAntiValues,IndexHoles,PrunedPages";
164+
+ "MvvRecords,MvvOverhead,MvvAntiValues,IndexHoles,PrunedPages,MarkedVersions";
161165

162166
}
163167

@@ -182,9 +186,28 @@ public void sawVersion(final long version, final int offset, final int valueLeng
182186

183187
private final VerifyVisitor _visitor = new VerifyVisitor() {
184188

189+
private long _currentPage;
190+
191+
@Override
192+
protected void visitPage(final long timestamp, final Volume volume, final long page, final int type,
193+
final int bufferSize, final int keyBlockStart, final int keyBlockEnd, final int alloc,
194+
final int available, final long rightSibling) throws PersistitException {
195+
_currentPage = page;
196+
}
197+
185198
@Override
186199
protected void visitDataRecord(final Key key, final int foundAt, final int tail, final int klength,
187200
final int offset, final int length, final byte[] bytes) throws PersistitException {
201+
/*
202+
* Leftover prune marks read normally (the length accessors strip
203+
* the mark bit), so they must be counted before visitAllVersions
204+
* and reported explicitly - see issue #290.
205+
*/
206+
final int marked = MVV.countMarkedVersions(bytes, offset, length);
207+
if (marked > 0) {
208+
_counters._markedVersionCount += marked;
209+
addFault("MVV has " + plural(marked, "version") + " with a leftover mark bit", _currentPage, 0, foundAt);
210+
}
188211
MVV.visitAllVersions(_versionVisitor, bytes, offset, length);
189212
if (_versionVisitor._count > 0) {
190213
_counters._mvvCount++;
@@ -544,6 +567,16 @@ public long getMvvAntiValues() {
544567
return _counters._mvvAntiValues;
545568
}
546569

570+
/**
571+
* @return Count of MVV versions whose length field still carries a
572+
* leftover prune mark bit, left behind by a prune interrupted
573+
* before the issue #286 fix. Such versions read normally but
574+
* indicate the volume was corrupted; pruning rewrites them.
575+
*/
576+
public long getMarkedVersionCount() {
577+
return _counters._markedVersionCount;
578+
}
579+
547580
/**
548581
* @return Count of pages for which an expected index pointer is missing
549582
*/

persistit/core/src/main/java/com/persistit/MVV.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,37 @@ static boolean verify(final byte[] bytes, final int offset, final int length) {
625625
return true;
626626
}
627627

628+
/**
629+
* Count versions whose length field still carries the mark bit. Marks are
630+
* transient state private to {@link #prune} and must never be observable
631+
* outside it; a non-zero count on a stored MVV means the value was
632+
* corrupted by a prune interrupted before the issue #286 fix. The length
633+
* accessors strip the mark bit silently, so such versions read normally
634+
* and only this scan can detect them (issue #290).
635+
*
636+
* @param bytes
637+
* the byte array
638+
* @param offset
639+
* the index of the first byte of the MVV within the byte array
640+
* @param length
641+
* the count of bytes in the MVV
642+
* @return the number of marked versions, 0 for a non-MVV value
643+
*/
644+
static int countMarkedVersions(final byte[] bytes, final int offset, final int length) {
645+
if (!isArrayMVV(bytes, offset, length)) {
646+
return 0;
647+
}
648+
int marked = 0;
649+
int from = offset + 1;
650+
while (from + LENGTH_PER_VERSION <= offset + length) {
651+
if (isMarked(bytes, from)) {
652+
marked++;
653+
}
654+
from += getLength(bytes, from) + LENGTH_PER_VERSION;
655+
}
656+
return marked;
657+
}
658+
628659
static boolean verify(final TransactionIndex ti, final byte[] bytes, final int offset, final int length) {
629660
if (!isArrayMVV(bytes, offset, length)) {
630661
/*

persistit/core/src/test/java/com/persistit/IntegrityCheckTest.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,56 @@ public void testBrokenMVVs() throws Exception {
9999
assertTrue(icheck.getFaults().length > 0);
100100
}
101101

102+
/**
103+
* Issue #290: a leftover prune mark bit (pre-#288 interrupted-prune
104+
* corruption) reads normally through all fetch paths, so IntegrityCheck
105+
* must detect it explicitly and report it as a fault.
106+
*/
107+
@Test
108+
public void testLeftoverMarkBitsReported() throws Exception {
109+
final Exchange ex = _persistit.getExchange(_volumeName, "mvv", true);
110+
disableBackgroundCleanup();
111+
transactionalStore(ex);
112+
113+
IntegrityCheck icheck = icheck();
114+
icheck.checkTree(ex.getTree());
115+
assertEquals(0, icheck.getFaults().length);
116+
assertEquals(0, icheck.getMarkedVersionCount());
117+
118+
final int corrupted = corrupt5(ex);
119+
assertTrue(corrupted > 0);
120+
121+
icheck = icheck();
122+
icheck.checkTree(ex.getTree());
123+
assertEquals(corrupted, icheck.getFaults().length);
124+
assertEquals(corrupted, icheck.getMarkedVersionCount());
125+
}
126+
127+
/**
128+
* Issue #290: pruning rewrites marked versions, so icheck with pruning
129+
* enabled is the remediation path - a subsequent icheck must be clean.
130+
*/
131+
@Test
132+
public void testPruneClearsLeftoverMarkBits() throws Exception {
133+
final Exchange ex = _persistit.getExchange(_volumeName, "mvv", true);
134+
disableBackgroundCleanup();
135+
transactionalStore(ex);
136+
_persistit.getTransactionIndex().updateActiveTransactionCache();
137+
138+
assertTrue(corrupt5(ex) > 0);
139+
140+
IntegrityCheck icheck = icheck();
141+
icheck.setPruneEnabled(true);
142+
icheck.checkTree(ex.getTree());
143+
assertTrue(icheck.getMarkedVersionCount() > 0);
144+
assertTrue(icheck.getPrunedPagesCount() > 0);
145+
146+
icheck = icheck();
147+
icheck.checkTree(ex.getTree());
148+
assertEquals(0, icheck.getMarkedVersionCount());
149+
assertEquals(0, icheck.getFaults().length);
150+
}
151+
102152
@Test
103153
public void testIndexFixHoles() throws Exception {
104154
final Exchange ex = _persistit.getExchange(_volumeName, "mvv", true);
@@ -338,6 +388,31 @@ private void corrupt4(final Exchange ex) throws PersistitException {
338388
buffer.release();
339389
}
340390

391+
/**
392+
* Simulates the pre-#288 interrupted-prune corruption (issue #286) by
393+
* setting the leftover mark bit on the first version of some MVV values
394+
*
395+
* @param ex
396+
* @return the number of values corrupted
397+
* @throws PersistitException
398+
*/
399+
private int corrupt5(final Exchange ex) throws PersistitException {
400+
ex.ignoreMVCCFetch(true);
401+
ex.clear().to(key(500));
402+
int corrupted = 0;
403+
while (corrupted < 10 && ex.next()) {
404+
final byte[] bytes = ex.getValue().getEncodedBytes();
405+
final int length = ex.getValue().getEncodedSize();
406+
if (MVV.isArrayMVV(bytes, 0, length)) {
407+
MVV.mark(bytes, 1);
408+
ex.store();
409+
corrupted++;
410+
}
411+
}
412+
ex.ignoreMVCCFetch(false);
413+
return corrupted;
414+
}
415+
341416
private IntegrityCheck icheck() {
342417
final IntegrityCheck icheck = new IntegrityCheck(_persistit);
343418
icheck.setMessageLogVerbosity(Task.LOG_VERBOSE);

persistit/core/src/test/java/com/persistit/MVVTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,34 @@ public void pruneExceptionUnmarksVersionsAtNonZeroOffset() throws Exception {
587587
assertArrayEquals("prune must leave the byte array unchanged when it throws", before, bytes);
588588
}
589589

590+
/**
591+
* Issue #290: leftover mark bits are invisible to the read paths (the
592+
* length accessors strip them), so IntegrityCheck needs a dedicated scan
593+
* to detect them.
594+
*/
595+
@Test
596+
public void countMarkedVersions() {
597+
final byte[] source = newArray(TYPE_MVV, 0, 0, 0, 0, 0, 0, 0, 10, 0, 2, 0xA, 0xB, 0, 0, 0, 0, 0, 0, 0, 11, 0,
598+
3, 0xC, 0xD, 0xE);
599+
assertEquals(0, MVV.countMarkedVersions(source, 0, source.length));
600+
MVV.mark(source, 1);
601+
assertEquals(1, MVV.countMarkedVersions(source, 0, source.length));
602+
MVV.mark(source, 13);
603+
assertEquals(2, MVV.countMarkedVersions(source, 0, source.length));
604+
MVV.unmark(source, 1);
605+
assertEquals(1, MVV.countMarkedVersions(source, 0, source.length));
606+
MVV.unmark(source, 13);
607+
assertEquals(0, MVV.countMarkedVersions(source, 0, source.length));
608+
}
609+
610+
@Test
611+
public void countMarkedVersionsNonMVV() {
612+
final byte[] source = newArray(0xA, 0xB, 0xC);
613+
assertEquals(0, MVV.countMarkedVersions(source, 0, source.length));
614+
assertEquals(0, MVV.countMarkedVersions(source, 0, 0));
615+
assertEquals(0, MVV.countMarkedVersions(source, 0, -1));
616+
}
617+
590618
//
591619
// Test helper methods
592620
//

0 commit comments

Comments
 (0)