Skip to content

Commit 79b2cd7

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. The scan runs only after visitAllVersions has validated the version structure, so a structurally corrupt MVV cannot produce a bogus marked-version fault. icheck -p remains the remediation: pruning rewrites the marked versions (issue #292 makes prune clear stale marks unconditionally). With icheck -P the marked-version faults are reported but do not block clearing the TransactionIndex, since the prune pass has already rewritten the marks. The icheck -c CSV example in Management.rst is updated for the new MarkedVersions column. Fixes #290
1 parent 14f35f8 commit 79b2cd7

5 files changed

Lines changed: 275 additions & 25 deletions

File tree

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

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public class IntegrityCheck extends Task {
6060

6161
private Volume _currentVolume;
6262
private Tree _currentTree;
63+
private long _currentPage;
6364
private LongBitSet _usedPageBits = new LongBitSet();
6465
private long _totalPages = 0;
6566
private long _pagesVisited = 0;
@@ -79,6 +80,7 @@ public class IntegrityCheck extends Task {
7980
private boolean _csv;
8081

8182
private final ArrayList<Fault> _faults = new ArrayList<Fault>();
83+
private int _markedVersionFaultCount;
8284
private final ArrayList<CleanupIndexHole> _holes = new ArrayList<CleanupIndexHole>();
8385

8486
// Used in checking long values
@@ -98,6 +100,7 @@ private static class Counters {
98100
private long _mvvCount = 0;
99101
private long _mvvOverhead = 0;
100102
private long _mvvAntiValues = 0;
103+
private long _markedVersionCount = 0;
101104
private long _pruningErrorCount = 0;
102105
private long _prunedPageCount = 0;
103106
private long _garbagePageCount = 0;
@@ -118,6 +121,7 @@ private static class Counters {
118121
_mvvCount = counters._mvvCount;
119122
_mvvOverhead = counters._mvvOverhead;
120123
_mvvAntiValues = counters._mvvAntiValues;
124+
_markedVersionCount = counters._markedVersionCount;
121125
_pruningErrorCount = counters._pruningErrorCount;
122126
_prunedPageCount = counters._prunedPageCount;
123127
_garbagePageCount = counters._garbagePageCount;
@@ -135,6 +139,7 @@ void difference(final Counters counters) {
135139
_mvvCount = counters._mvvCount - _mvvCount;
136140
_mvvOverhead = counters._mvvOverhead - _mvvOverhead;
137141
_mvvAntiValues = counters._mvvAntiValues - _mvvAntiValues;
142+
_markedVersionCount = counters._markedVersionCount - _markedVersionCount;
138143
_pruningErrorCount = counters._pruningErrorCount - _pruningErrorCount;
139144
_prunedPageCount = counters._prunedPageCount - _prunedPageCount;
140145
_garbagePageCount = counters._garbagePageCount - _garbagePageCount;
@@ -144,20 +149,21 @@ void difference(final Counters counters) {
144149
public String toString() {
145150
return String.format("Index pages/bytes: %,d / %,d Data pages/bytes: %,d / %,d"
146151
+ " 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);
152+
+ "%,d / %,d / %,d / %,d Holes %,d Pages pruned %,d Marked versions %,d", _indexPageCount,
153+
_indexBytesInUse, _dataPageCount, _dataBytesInUse, _longRecordPageCount, _longRecordBytesInUse,
154+
_mvvPageCount, _mvvCount, _mvvOverhead, _mvvAntiValues, _indexHoleCount, _prunedPageCount,
155+
_markedVersionCount);
150156
}
151157

152158
private String toCSV() {
153-
return String.format("%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d", _indexPageCount, _indexBytesInUse,
159+
return String.format("%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d", _indexPageCount, _indexBytesInUse,
154160
_dataPageCount, _dataBytesInUse, _longRecordPageCount, _longRecordBytesInUse, _mvvPageCount,
155-
_mvvCount, _mvvOverhead, _mvvAntiValues, _indexHoleCount, _prunedPageCount);
161+
_mvvCount, _mvvOverhead, _mvvAntiValues, _indexHoleCount, _prunedPageCount, _markedVersionCount);
156162
}
157163

158164
private final static String CSV_HEADERS = "IndexPages,IndexBytes,"
159165
+ "DataPages,DataBytes,LongRecordPages,LongRecordBytes,MvvPages,"
160-
+ "MvvRecords,MvvOverhead,MvvAntiValues,IndexHoles,PrunedPages";
166+
+ "MvvRecords,MvvOverhead,MvvAntiValues,IndexHoles,PrunedPages,MarkedVersions";
161167

162168
}
163169

@@ -186,6 +192,20 @@ public void sawVersion(final long version, final int offset, final int valueLeng
186192
protected void visitDataRecord(final Key key, final int foundAt, final int tail, final int klength,
187193
final int offset, final int length, final byte[] bytes) throws PersistitException {
188194
MVV.visitAllVersions(_versionVisitor, bytes, offset, length);
195+
/*
196+
* Scanned only after visitAllVersions has validated the version
197+
* structure. Leftover prune marks read normally (the length
198+
* accessors strip the mark bit), so only this dedicated scan can
199+
* detect and report them - see issue #290.
200+
*/
201+
final int marked = MVV.countMarkedVersions(bytes, offset, length);
202+
if (marked > 0) {
203+
_counters._markedVersionCount += marked;
204+
if (addFault("MVV has " + plural(marked, "version") + " with a leftover mark bit", _currentPage, 0,
205+
foundAt)) {
206+
_markedVersionFaultCount++;
207+
}
208+
}
189209
if (_versionVisitor._count > 0) {
190210
_counters._mvvCount++;
191211
final int voffset = _versionVisitor._lastOffset;
@@ -289,7 +309,14 @@ protected void runTask() {
289309
postMessage("Total " + toString(), LOG_NORMAL);
290310
}
291311
if (_pruneAndClear) {
292-
if (_faults.isEmpty() && _counters._mvvPageCount == _counters._prunedPageCount
312+
/*
313+
* Leftover mark-bit faults (issue #290) identify volumes
314+
* affected by a pre-#286 interrupted prune but do not block
315+
* clearing the TransactionIndex: the prune pass above has
316+
* already rewritten those marks (issue #292). Only other
317+
* faults and incomplete pruning count as failures here.
318+
*/
319+
if (_faults.size() == _markedVersionFaultCount && _counters._mvvPageCount == _counters._prunedPageCount
293320
&& _counters._pruningErrorCount == 0) {
294321
final int count = _persistit.getTransactionIndex().resetMVVCounts(startTimestamp);
295322
postMessage(String.format("%,d aborted transactions were cleared by pruning", count), LOG_NORMAL);
@@ -329,11 +356,13 @@ private String plural(final int n, final String m) {
329356
}
330357
}
331358

332-
private void addFault(final String description, final long page, final int level, final int position) {
359+
private boolean addFault(final String description, final long page, final int level, final int position) {
333360
final Fault fault = new Fault(resourceName(), this, description, page, _treeDepth, level, position);
334-
if (_faults.size() < MAX_FAULTS)
361+
final boolean recorded = _faults.size() < MAX_FAULTS;
362+
if (recorded)
335363
_faults.add(fault);
336364
postMessage(fault.toString(), LOG_VERBOSE);
365+
return recorded;
337366
}
338367

339368
private void addGarbageFault(final String description, final long page, final int level, final int position) {
@@ -544,6 +573,16 @@ public long getMvvAntiValues() {
544573
return _counters._mvvAntiValues;
545574
}
546575

576+
/**
577+
* @return Count of MVV versions whose length field still carries a
578+
* leftover prune mark bit, left behind by a prune interrupted
579+
* before the issue #286 fix. Such versions read normally but
580+
* indicate the volume was corrupted; pruning rewrites them.
581+
*/
582+
public long getMarkedVersionCount() {
583+
return _counters._markedVersionCount;
584+
}
585+
547586
/**
548587
* @return Count of pages for which an expected index pointer is missing
549588
*/
@@ -1144,6 +1183,7 @@ private Buffer walkRight(final int level, final long toPage, final Key key, fina
11441183
}
11451184

11461185
private boolean verifyPage(final Buffer buffer, final long page, final int level, final Key key, final Tree tree) {
1186+
_currentPage = page;
11471187
if (buffer.getPageAddress() != page) {
11481188
addFault("Buffer contains wrong page " + buffer.getPageAddress(), page, level, 0);
11491189
return false;

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

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

646+
/**
647+
* Count versions whose length field still carries the mark bit. Marks are
648+
* transient state private to {@link #prune} and must never be observable
649+
* outside it; a non-zero count on a stored MVV means the value was
650+
* corrupted by a prune interrupted before the issue #286 fix. The length
651+
* accessors strip the mark bit silently, so such versions read normally
652+
* and only this scan can detect them (issue #290).
653+
*
654+
* @param bytes
655+
* the byte array
656+
* @param offset
657+
* the index of the first byte of the MVV within the byte array
658+
* @param length
659+
* the count of bytes in the MVV
660+
* @return the number of marked versions, 0 for a non-MVV value
661+
*/
662+
static int countMarkedVersions(final byte[] bytes, final int offset, final int length) {
663+
if (!isArrayMVV(bytes, offset, length)) {
664+
return 0;
665+
}
666+
int marked = 0;
667+
int from = offset + 1;
668+
while (from + LENGTH_PER_VERSION <= offset + length) {
669+
if (isMarked(bytes, from)) {
670+
marked++;
671+
}
672+
from += getLength(bytes, from) + LENGTH_PER_VERSION;
673+
}
674+
return marked;
675+
}
676+
646677
static boolean verify(final TransactionIndex ti, final byte[] bytes, final int offset, final int length) {
647678
if (!isArrayMVV(bytes, offset, length)) {
648679
/*

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

Lines changed: 164 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@
2323
import org.junit.Test;
2424

2525
import java.io.PrintWriter;
26+
import java.io.StringWriter;
27+
import java.util.SortedMap;
28+
import java.util.TreeMap;
2629

2730
import static org.junit.Assert.assertEquals;
31+
import static org.junit.Assert.assertFalse;
2832
import static org.junit.Assert.assertNotNull;
2933
import static org.junit.Assert.assertTrue;
3034

@@ -99,6 +103,88 @@ public void testBrokenMVVs() throws Exception {
99103
assertTrue(icheck.getFaults().length > 0);
100104
}
101105

106+
/**
107+
* Issue #290: a leftover prune mark bit (pre-#288 interrupted-prune
108+
* corruption) reads normally through all fetch paths, so IntegrityCheck
109+
* must detect it explicitly and report it as a fault.
110+
*/
111+
@Test
112+
public void testLeftoverMarkBitsReported() throws Exception {
113+
final Exchange ex = _persistit.getExchange(_volumeName, "mvv", true);
114+
disableBackgroundCleanup();
115+
transactionalStore(ex);
116+
117+
IntegrityCheck icheck = icheck();
118+
icheck.checkTree(ex.getTree());
119+
assertEquals(0, icheck.getFaults().length);
120+
assertEquals(0, icheck.getMarkedVersionCount());
121+
122+
final int[] corrupted = corrupt5(ex);
123+
final int corruptedValues = corrupted[0];
124+
final int markedVersions = corrupted[1];
125+
assertTrue(corruptedValues > 0);
126+
assertTrue("some values must carry more than one mark", markedVersions > corruptedValues);
127+
128+
icheck = icheck();
129+
icheck.checkTree(ex.getTree());
130+
assertEquals(corruptedValues, icheck.getFaults().length);
131+
assertEquals(markedVersions, icheck.getMarkedVersionCount());
132+
}
133+
134+
/**
135+
* Issue #290: pruning rewrites marked versions, so icheck with pruning
136+
* enabled is the remediation path - a subsequent icheck must be clean.
137+
*/
138+
@Test
139+
public void testPruneClearsLeftoverMarkBits() throws Exception {
140+
final Exchange ex = _persistit.getExchange(_volumeName, "mvv", true);
141+
disableBackgroundCleanup();
142+
transactionalStore(ex);
143+
_persistit.getTransactionIndex().updateActiveTransactionCache();
144+
145+
final SortedMap<String, String> expectedContents = treeContents(ex);
146+
assertTrue(corrupt5(ex)[0] > 0);
147+
148+
IntegrityCheck icheck = icheck();
149+
icheck.setPruneEnabled(true);
150+
icheck.checkTree(ex.getTree());
151+
assertTrue(icheck.getMarkedVersionCount() > 0);
152+
assertTrue(icheck.getPrunedPagesCount() > 0);
153+
154+
assertEquals("remediation must not change the visible contents", expectedContents, treeContents(ex));
155+
156+
icheck = icheck();
157+
icheck.checkTree(ex.getTree());
158+
assertEquals(0, icheck.getMarkedVersionCount());
159+
assertEquals(0, icheck.getFaults().length);
160+
}
161+
162+
/**
163+
* Issues #290/#292: icheck -P reports leftover mark bits as faults but
164+
* must still clear the TransactionIndex - the prune pass has already
165+
* rewritten the marks, so they are not a pruning failure.
166+
*/
167+
@Test
168+
public void testPruneAndClearWithLeftoverMarkBits() throws Exception {
169+
final Exchange ex = _persistit.getExchange(_volumeName, "mvv", true);
170+
disableBackgroundCleanup();
171+
transactionalStore(ex);
172+
_persistit.getTransactionIndex().updateActiveTransactionCache();
173+
174+
assertTrue(corrupt5(ex)[0] > 0);
175+
176+
final IntegrityCheck icheck = (IntegrityCheck) CLI.parseTask(_persistit, "icheck trees=* -u -P");
177+
final StringWriter output = new StringWriter();
178+
icheck.setMessageWriter(new PrintWriter(output));
179+
icheck.setup(1, "icheck", "cli", 0, 5);
180+
icheck.run();
181+
182+
assertTrue(icheck.getMarkedVersionCount() > 0);
183+
assertTrue(icheck.hasFaults());
184+
assertTrue(output.toString(), output.toString().contains("aborted transactions were cleared by pruning"));
185+
assertFalse(output.toString(), output.toString().contains("PruneAndClear failed"));
186+
}
187+
102188
@Test
103189
public void testIndexFixHoles() throws Exception {
104190
final Exchange ex = _persistit.getExchange(_volumeName, "mvv", true);
@@ -283,26 +369,52 @@ private void corrupt1(final Exchange ex) throws PersistitException {
283369
buffer.release();
284370
}
285371

372+
private interface MVVCorruption {
373+
/** Corrupts one stored MVV value; returns its contribution to the total */
374+
int corrupt(byte[] bytes, int length);
375+
}
376+
286377
/**
287-
* Corrupts some MVV values on a page by damaging a version length field
288-
*
378+
* Applies a corruption to the raw bytes of up to ten stored MVV values
379+
*
289380
* @param ex
381+
* @param corruption
382+
* @return the number of values corrupted and the summed corruption
383+
* contributions, as a two-element array
290384
* @throws PersistitException
291385
*/
292-
private void corrupt2(final Exchange ex) throws PersistitException {
386+
private int[] corruptMVVs(final Exchange ex, final MVVCorruption corruption) throws PersistitException {
293387
ex.ignoreMVCCFetch(true);
294-
ex.clear().to(key(500));
295-
int corrupted = 0;
296-
while (corrupted < 10 && ex.next()) {
297-
final byte[] bytes = ex.getValue().getEncodedBytes();
298-
final int length = ex.getValue().getEncodedSize();
299-
if (MVV.isArrayMVV(bytes, 0, length)) {
300-
bytes[9]++;
301-
ex.store();
302-
corrupted++;
388+
try {
389+
ex.clear().to(key(500));
390+
int corrupted = 0;
391+
int total = 0;
392+
while (corrupted < 10 && ex.next()) {
393+
final byte[] bytes = ex.getValue().getEncodedBytes();
394+
final int length = ex.getValue().getEncodedSize();
395+
if (MVV.isArrayMVV(bytes, 0, length)) {
396+
total += corruption.corrupt(bytes, length);
397+
ex.store();
398+
corrupted++;
399+
}
303400
}
401+
return new int[] { corrupted, total };
402+
} finally {
403+
ex.ignoreMVCCFetch(false);
304404
}
305-
ex.ignoreMVCCFetch(false);
405+
}
406+
407+
/**
408+
* Corrupts some MVV values on a page by damaging a version length field
409+
*
410+
* @param ex
411+
* @throws PersistitException
412+
*/
413+
private void corrupt2(final Exchange ex) throws PersistitException {
414+
corruptMVVs(ex, (bytes, length) -> {
415+
bytes[9]++;
416+
return 1;
417+
});
306418
}
307419

308420
/**
@@ -338,6 +450,45 @@ private void corrupt4(final Exchange ex) throws PersistitException {
338450
buffer.release();
339451
}
340452

453+
/**
454+
* Simulates the pre-#288 interrupted-prune corruption (issue #286) by
455+
* setting the leftover mark bit on every version of some MVV values
456+
*
457+
* @param ex
458+
* @return the number of values corrupted and the total number of versions
459+
* marked, as a two-element array
460+
* @throws PersistitException
461+
*/
462+
private int[] corrupt5(final Exchange ex) throws PersistitException {
463+
return corruptMVVs(ex, (bytes, length) -> {
464+
int marked = 0;
465+
int from = 1;
466+
while (from + MVV.LENGTH_PER_VERSION <= length) {
467+
MVV.mark(bytes, from);
468+
marked++;
469+
from += MVV.getLength(bytes, from) + MVV.LENGTH_PER_VERSION;
470+
}
471+
return marked;
472+
});
473+
}
474+
475+
/**
476+
* Returns the visible key/value contents of the tree as seen by a
477+
* non-transactional traversal
478+
*
479+
* @param ex
480+
* @return the visible contents, keyed by decoded key string
481+
* @throws PersistitException
482+
*/
483+
private SortedMap<String, String> treeContents(final Exchange ex) throws PersistitException {
484+
final SortedMap<String, String> contents = new TreeMap<String, String>();
485+
ex.clear().append(Key.BEFORE);
486+
while (ex.next()) {
487+
contents.put(ex.getKey().decodeString(), ex.getValue().getString());
488+
}
489+
return contents;
490+
}
491+
341492
private IntegrityCheck icheck() {
342493
final IntegrityCheck icheck = new IntegrityCheck(_persistit);
343494
icheck.setMessageLogVerbosity(Task.LOG_VERBOSE);

0 commit comments

Comments
 (0)