Skip to content

Commit 63ca4db

Browse files
committed
Clear stale MVV mark bits before pruning
MVV.prune uses the 0x8000 mark bit in the version length field as transient private state and assumes no version is marked on entry. A volume corrupted by a prune interrupted before the #288 fix violates that assumption: a leftover mark on an obsolete version won the primordial-conversion scan, resurrecting the old value (or an undefined value) while silently dropping the most recent committed version with no PrunedVersion accounting for it — leaking the MVV count and, for long records, the page chain. In the multi-version path a stale-marked dead version survived one extra prune round with its accounting deferred. Sweep all mark bits in the region clean before the first pass, so mark state is guaranteed consistent regardless of what is on disk. The sweep never touches bytes outside the MVV region even on corrupt input. Fixes #292
1 parent fff316f commit 63ca4db

2 files changed

Lines changed: 151 additions & 1 deletion

File tree

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,11 @@ else if (target[targetOffset] != TYPE_MVV_BYTE) {
381381
* </p>
382382
* <p>
383383
* This method leaves the byte array unchanged if any of its checked
384-
* Exceptions is thrown.
384+
* Exceptions is thrown, with one exception: stale mark bits left on disk
385+
* by a prune interrupted before the issue #286 fix are cleared up front
386+
* regardless of outcome (issue #292). Mark bits are transient state
387+
* private to this method and invisible to the read paths, so clearing
388+
* them repairs the corruption without altering any version's value.
385389
* </p>
386390
* <p>
387391
* This method adds {@link PrunedVersion} instances to the supplied list.
@@ -437,6 +441,20 @@ static int prune(final byte[] bytes, final int offset, final int length, final T
437441
long lastVersionHandle = Long.MIN_VALUE;
438442
long lastVersionTc = UNCOMMITTED;
439443
long uncommittedTransactionTs = 0;
444+
/*
445+
* The passes below trust isMarked() while the marked counter only
446+
* counts marks set by this run, so a stale mark left on disk by a
447+
* prune interrupted before the issue #286 fix would win the
448+
* primordial-conversion scan — promoting an obsolete version and
449+
* silently dropping the current one with no PrunedVersion
450+
* accounting (issue #292). Sweep the region clean before the
451+
* first pass marks the real keepers.
452+
*/
453+
while (from + LENGTH_PER_VERSION <= offset + length) {
454+
unmark(bytes, from);
455+
from += getLength(bytes, from) + LENGTH_PER_VERSION;
456+
}
457+
from = offset + 1;
440458
/*
441459
* First pass - mark all the versions to keep. Keep every
442460
* UNCOMMITTED version (there may be more than one created by the

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

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import static com.persistit.MVV.TYPE_MVV;
3434
import static org.junit.Assert.assertArrayEquals;
3535
import static org.junit.Assert.assertEquals;
36+
import static org.junit.Assert.assertFalse;
3637
import static org.junit.Assert.assertTrue;
3738
import static org.junit.Assert.fail;
3839

@@ -624,10 +625,141 @@ public void pruneExceptionMustNotUnmarkPastRegionEnd() throws Exception {
624625
assertArrayEquals("prune must not write outside the MVV region when it throws", before, bytes);
625626
}
626627

628+
//
629+
// Issue #292: prune uses the mark bit as private transient state and
630+
// assumes no version is marked on entry. A stale mark left on disk by a
631+
// prune interrupted before the issue #286 fix violated that assumption:
632+
// prune could promote the wrong version and skip the PrunedVersion
633+
// accounting. Prune now clears all mark bits up front.
634+
//
635+
636+
/**
637+
* A stale mark on an obsolete committed version used to win the
638+
* primordial-conversion scan: the obsolete value was resurrected while the
639+
* most recent committed version was silently dropped — and neither showed
640+
* up in the PrunedVersion list, leaking the MVV count (and the page chain,
641+
* had the dropped version been a long record).
642+
*/
643+
@Test
644+
public void pruneStaleMarkedVersionPromotesMostRecentCommitted() throws Exception {
645+
final TimestampAllocator tsa = new TimestampAllocator();
646+
final TransactionIndex ti = new TransactionIndex(tsa, 1);
647+
648+
final int offset = 7;
649+
final byte[] bytes = new byte[offset + 100];
650+
final byte[] oldValue = { 0xA, 0xB };
651+
final byte[] newValue = { 0xC, 0xD, 0xE };
652+
int length = storeCommittedVersion(tsa, ti, bytes, offset, -1, oldValue);
653+
final long oldVersionHandle = MVV.getVersion(bytes, offset + 1);
654+
length = storeCommittedVersion(tsa, ti, bytes, offset, length, newValue);
655+
ti.updateActiveTransactionCache();
656+
657+
/* The stale mark an interrupted pre-#286 prune leaves behind. */
658+
MVV.mark(bytes, offset + 1);
659+
660+
final ArrayList<MVV.PrunedVersion> pruned = new ArrayList<MVV.PrunedVersion>();
661+
final int newLength = MVV.prune(bytes, offset, length, ti, true, pruned);
662+
663+
assertEquals(newValue.length, newLength);
664+
assertArrayEquals(newValue, Arrays.copyOfRange(bytes, offset, offset + newLength));
665+
assertEquals("obsolete version must be accounted for", 1, pruned.size());
666+
assertEquals(oldVersionHandle, pruned.get(0).getVersionHandle());
667+
}
668+
669+
/**
670+
* The same scenario with the stale mark on the zero-length initial version
671+
* of a value that was undefined before the MVV was created: prune used to
672+
* resurrect "undefined", discarding the committed value entirely.
673+
*/
674+
@Test
675+
public void pruneStaleMarkedUndefinedVersionNotPromoted() throws Exception {
676+
final TimestampAllocator tsa = new TimestampAllocator();
677+
final TransactionIndex ti = new TransactionIndex(tsa, 1);
678+
679+
final int offset = 7;
680+
final byte[] bytes = new byte[offset + 100];
681+
final byte[] value = { 0xC, 0xD, 0xE };
682+
final int length = storeCommittedVersion(tsa, ti, bytes, offset, 0, value);
683+
ti.updateActiveTransactionCache();
684+
685+
/* The stale mark sits on the zero-length undefined initial version. */
686+
MVV.mark(bytes, offset + 1);
687+
688+
final ArrayList<MVV.PrunedVersion> pruned = new ArrayList<MVV.PrunedVersion>();
689+
final int newLength = MVV.prune(bytes, offset, length, ti, true, pruned);
690+
691+
assertEquals(value.length, newLength);
692+
assertArrayEquals(value, Arrays.copyOfRange(bytes, offset, offset + newLength));
693+
assertEquals("the primordial version carries no accounting", 0, pruned.size());
694+
}
695+
696+
/**
697+
* Milder variant in the multi-version path: a stale-marked dead version
698+
* was treated as a keeper — it survived the prune and was left out of the
699+
* PrunedVersion list, deferring its removal and accounting to the next
700+
* prune while this one reported a clean result.
701+
*/
702+
@Test
703+
public void pruneStaleMarkedVersionPrunedInMultiVersionPath() throws Exception {
704+
final TimestampAllocator tsa = new TimestampAllocator();
705+
final TransactionIndex ti = new TransactionIndex(tsa, 1);
706+
707+
final int offset = 7;
708+
final byte[] bytes = new byte[offset + 100];
709+
final byte[] v1 = { 0xA };
710+
final byte[] v2 = { 0xB, 0xC };
711+
final byte[] v3 = { 0xD, 0xE, 0xF };
712+
int length = storeCommittedVersion(tsa, ti, bytes, offset, -1, v1);
713+
final long vh1 = MVV.getVersion(bytes, offset + 1);
714+
length = storeCommittedVersion(tsa, ti, bytes, offset, length, v2);
715+
final int v2At = offset + 1 + MVV.LENGTH_PER_VERSION + v1.length;
716+
final long vh2 = MVV.getVersion(bytes, v2At);
717+
length = storeCommittedVersion(tsa, ti, bytes, offset, length, v3);
718+
final int v3At = v2At + MVV.LENGTH_PER_VERSION + v2.length;
719+
final long vh3 = MVV.getVersion(bytes, v3At);
720+
ti.updateActiveTransactionCache();
721+
722+
MVV.mark(bytes, offset + 1);
723+
724+
final ArrayList<MVV.PrunedVersion> pruned = new ArrayList<MVV.PrunedVersion>();
725+
final int newLength = MVV.prune(bytes, offset, length, ti, false, pruned);
726+
727+
assertEquals("both dead versions must be pruned in one round", MVV.overheadLength(1) + v3.length, newLength);
728+
assertEquals(vh3, MVV.getVersion(bytes, offset + 1));
729+
assertEquals(v3.length, MVV.getLength(bytes, offset + 1));
730+
assertArrayEquals(v3,
731+
Arrays.copyOfRange(bytes, offset + 1 + MVV.LENGTH_PER_VERSION, offset + 1 + MVV.LENGTH_PER_VERSION
732+
+ v3.length));
733+
assertEquals("both dead versions must be accounted for", 2, pruned.size());
734+
assertEquals(vh1, pruned.get(0).getVersionHandle());
735+
assertEquals(vh2, pruned.get(1).getVersionHandle());
736+
int from = offset + 1;
737+
while (from + MVV.LENGTH_PER_VERSION <= offset + newLength) {
738+
assertFalse("no version may remain marked", MVV.isMarked(bytes, from));
739+
from += MVV.getLength(bytes, from) + MVV.LENGTH_PER_VERSION;
740+
}
741+
}
742+
627743
//
628744
// Test helper methods
629745
//
630746

747+
/**
748+
* Store <code>value</code> as a new version created by a registered
749+
* transaction and commit it, so that {@link MVV#prune} sees a committed,
750+
* non-concurrent version.
751+
*/
752+
private static int storeCommittedVersion(final TimestampAllocator tsa, final TransactionIndex ti,
753+
final byte[] bytes, final int offset, final int length, final byte[] value) throws Exception {
754+
final TransactionStatus status = ti.registerTransaction();
755+
final int newLength = MVV.storeVersion(bytes, offset, length, bytes.length,
756+
TransactionIndex.ts2vh(status.getTs()), value, 0, value.length) & STORE_LENGTH_MASK;
757+
final long tc = tsa.updateTimestamp();
758+
status.commit(tc);
759+
ti.notifyCompleted(status, tc);
760+
return newLength;
761+
}
762+
631763
private static int writeArray(final byte[] array, final int... contents) {
632764
assert contents.length <= array.length : "Too many values for array";
633765
for (int i = 0; i < contents.length; ++i) {

0 commit comments

Comments
 (0)