diff --git a/pkg/storage/mvcc.go b/pkg/storage/mvcc.go index ff9a91e04b53..75c5db9dc11e 100644 --- a/pkg/storage/mvcc.go +++ b/pkg/storage/mvcc.go @@ -699,13 +699,6 @@ func updateStatsOnResolve( return ms } - // An intent can't turn from deleted to non-deleted and vice versa while being - // resolved. - if orig.Deleted != meta.Deleted { - log.Fatalf(context.TODO(), "on resolve, original meta was deleted=%t, but new one is deleted=%t", - orig.Deleted, meta.Deleted) - } - // In the main case, we had an old intent at orig.Timestamp, and a new intent // or value at meta.Timestamp. We'll walk through the contributions below, // taking special care for LockAge and GCBytesAge. @@ -724,14 +717,16 @@ func updateStatsOnResolve( ms.KeyBytes -= origMetaKeySize + orig.KeyBytes ms.ValBytes -= origMetaValSize + orig.ValBytes - // If the old intent is a deletion, then the key already isn't tracked - // in LiveBytes any more (and the new intent/value is also a deletion). - // If we're looking at a non-deletion intent/value, update the live - // bytes to account for the difference between the previous intent and - // the new intent/value. - if !meta.Deleted { + // Next, we adjust LiveBytes based on meta.Deleted and orig.Deleted. + // Note that LiveBytes here corresponds to ts = orig.Timestamp.WallTime. + // LiveBytes at ts = meta.Timestamp.WallTime is adjusted below. + // If the original value was deleted, there is no need to adjust the + // contribution of the original key and value to LiveBytes. Otherwise, we need + // to subtract the original key and value's contribution from LiveBytes. + if !orig.Deleted { ms.LiveBytes -= origMetaKeySize + origMetaValSize ms.LiveBytes -= orig.KeyBytes + orig.ValBytes + ms.LiveCount-- } // LockAge is always accrued from the intent's own timestamp on. @@ -768,6 +763,7 @@ func updateStatsOnResolve( // The new meta key appears. if !meta.Deleted { ms.LiveBytes += (metaKeySize + metaValSize) + (meta.KeyBytes + meta.ValBytes) + ms.LiveCount++ } if !commit { @@ -5163,13 +5159,20 @@ func mvccResolveWriteIntent( // remains, the rolledBackVal is set to a non-nil value. var rolledBackVal *MVCCValue var err error + buf.newMeta = *meta + newMeta := &buf.newMeta if len(intent.IgnoredSeqNums) > 0 { // NOTE: mvccMaybeRewriteIntentHistory mutates its meta argument. // TODO(nvanbenschoten): this is an awkward interface. We shouldn't // be mutating meta and we shouldn't be restoring the previous value // here. Instead, this should all be handled down below. var removeIntent bool - removeIntent, rolledBackVal, err = mvccMaybeRewriteIntentHistory(ctx, writer, intent.IgnoredSeqNums, meta, latestKey) + // Instead of modifying meta, pass a copy of it (newMeta), which will be the + // starting point for the updated metadata. It's important to keep meta + // intact and corresponding to the stats in ms to ensure that later on (in + // updateStatsOnResolve) the stats will be updated correctly based on the + // old meta (meta) and the new meta (newMeta). + removeIntent, rolledBackVal, err = mvccMaybeRewriteIntentHistory(ctx, writer, intent.IgnoredSeqNums, newMeta, latestKey) if err != nil { return false, err } @@ -5211,9 +5214,6 @@ func mvccResolveWriteIntent( // is because removeIntent implies rolledBackVal == nil, pushed == false, and // commit == false. if commit || pushed || rolledBackVal != nil { - buf.newMeta = *meta - newMeta := &buf.newMeta - // The intent might be committing at a higher timestamp, or it might be // getting pushed. newTimestamp := intent.Txn.WriteTimestamp diff --git a/pkg/storage/mvcc_stats_test.go b/pkg/storage/mvcc_stats_test.go index 2454749f9564..901de1758526 100644 --- a/pkg/storage/mvcc_stats_test.go +++ b/pkg/storage/mvcc_stats_test.go @@ -1482,6 +1482,261 @@ func TestMVCCStatsSysPutPut(t *testing.T) { assertEqLocal(t, engine, "after second put", aggMS, &expMS) } +// TestMVCCStatsPutRollbackDelete exercises the case in which an intent is +// written, then re-written by a deletion tombstone, which is rolled back. We +// expect the stats to be adjusted correctly for the rolled back delete, and +// crucially, for the LiveBytes and LiveCount to be non-zero and corresponding +// to the original value. +func TestMVCCStatsPutRollbackDelete(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + engine := NewDefaultInMemForTesting() + defer engine.Close() + + ctx := context.Background() + aggMS := &enginepb.MVCCStats{} + assertEq(t, engine, "initially", aggMS, &enginepb.MVCCStats{}) + + key := roachpb.Key("a") + value := roachpb.MakeValueFromString("value") + ts := hlc.Timestamp{WallTime: 1e9} + txn := &roachpb.Transaction{ + TxnMeta: enginepb.TxnMeta{ID: uuid.MakeV4(), WriteTimestamp: ts}, + ReadTimestamp: ts, + } + + // Put a value. + if _, err := MVCCPut(ctx, engine, key, txn.ReadTimestamp, value, MVCCWriteOptions{Txn: txn, Stats: aggMS}); err != nil { + t.Fatal(err) + } + + mKeySize := int64(mvccKey(key).EncodedSize()) // 2 + mValSize := int64((&enginepb.MVCCMetadata{ // 46 + Timestamp: ts.ToLegacyTimestamp(), + Deleted: false, + Txn: &txn.TxnMeta, + }).Size()) + mValSize += 2 + vKeySize := MVCCVersionTimestampSize // 12 + vValSize := int64(len(value.RawBytes)) // 10 + if disableSimpleValueEncoding { + vValSize += emptyMVCCValueHeaderSize // 17 + } + + expMS := enginepb.MVCCStats{ + LastUpdateNanos: 1e9, + LiveBytes: mKeySize + mValSize + vKeySize + vValSize, // 2+(46[+2])+12+(10[+7]) = 68[+2][+7] + LiveCount: 1, + KeyBytes: mKeySize + vKeySize, // 2+12 =14 + KeyCount: 1, + ValBytes: mValSize + vValSize, // (46[+2])+(10[+7]) = 54[+2][+7] + ValCount: 1, + IntentCount: 1, + LockCount: 1, + IntentBytes: vKeySize + vValSize, // 12+(10[+7]) = 22[+7] + GCBytesAge: 0, + } + assertEq(t, engine, "after put", aggMS, &expMS) + + txn.Sequence++ + + // Annoyingly, the new meta value is actually a little larger thanks to the + // sequence number. Also since there was a write previously on the same + // transaction, the IntentHistory will add a few bytes to the metadata. + encValue, err := EncodeMVCCValue(MVCCValue{Value: value}) + require.NoError(t, err) + m2ValSize := int64((&enginepb.MVCCMetadata{ + Timestamp: ts.ToLegacyTimestamp(), + Deleted: true, + Txn: &txn.TxnMeta, + IntentHistory: []enginepb.MVCCMetadata_SequencedIntent{ + {Sequence: 0, Value: encValue}, + }, + }).Size()) + expM2ValSize := 64 + if disableSimpleValueEncoding { + expM2ValSize += int(emptyMVCCValueHeaderSize) + } + require.EqualValues(t, expM2ValSize, m2ValSize) + + // Delete the value. + if _, _, err := MVCCDelete(ctx, engine, key, txn.ReadTimestamp, MVCCWriteOptions{Txn: txn, Stats: aggMS}); err != nil { + t.Fatal(err) + } + + v2ValSize := int64(0) // tombstone + if disableSimpleValueEncoding { + v2ValSize += emptyMVCCValueHeaderSize // 7 + } + + expAggMS := enginepb.MVCCStats{ + LastUpdateNanos: 1e9, + LiveBytes: 0, + LiveCount: 0, + KeyCount: 1, + ValCount: 1, + KeyBytes: mKeySize + vKeySize, + ValBytes: m2ValSize + v2ValSize, + LockAge: 0, + IntentCount: 1, + LockCount: 1, + IntentBytes: vKeySize + v2ValSize, + GCBytesAge: 0, + } + + assertEq(t, engine, "after deleting", aggMS, &expAggMS) + + // Now commit the value and roll back the delete. + txn.Status = roachpb.COMMITTED + txn.AddIgnoredSeqNumRange(enginepb.IgnoredSeqNumRange{Start: 1, End: 1}) + if _, _, _, _, err := MVCCResolveWriteIntent(ctx, engine, aggMS, + roachpb.MakeLockUpdate(txn, roachpb.Span{Key: key}), + MVCCResolveWriteIntentOptions{}, + ); err != nil { + t.Fatal(err) + } + + expAggMS = enginepb.MVCCStats{ + LastUpdateNanos: 1e9, + LiveBytes: mKeySize + vKeySize + vValSize, + LiveCount: 1, // the key is live after the rollback + KeyCount: 1, + ValCount: 1, + KeyBytes: mKeySize + vKeySize, + ValBytes: vValSize, + GCBytesAge: 0, + } + + assertEq(t, engine, "after committing", aggMS, &expAggMS) +} + +// TestMVCCStatsDeleteRollbackPut exercises the case in which a deletion +// tombstone is written, then re-written by an intent, which is rolled back. We +// expect the stats to be adjusted correctly for the rolled back intent, and +// crucially, for the LiveBytes and LiveCount to be zero. +func TestMVCCStatsDeleteRollbackPut(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + engine := NewDefaultInMemForTesting() + defer engine.Close() + + ctx := context.Background() + aggMS := &enginepb.MVCCStats{} + assertEq(t, engine, "initially", aggMS, &enginepb.MVCCStats{}) + + key := roachpb.Key("a") + value := roachpb.MakeValueFromString("value") + ts := hlc.Timestamp{WallTime: 1e9} + txn := &roachpb.Transaction{ + TxnMeta: enginepb.TxnMeta{ID: uuid.MakeV4(), WriteTimestamp: ts}, + ReadTimestamp: ts, + } + + // Delete the value. + if _, _, err := MVCCDelete(ctx, engine, key, txn.ReadTimestamp, MVCCWriteOptions{Txn: txn, Stats: aggMS}); err != nil { + t.Fatal(err) + } + + mKeySize := int64(mvccKey(key).EncodedSize()) // 2 + mValSize := int64((&enginepb.MVCCMetadata{ // 46 + Timestamp: ts.ToLegacyTimestamp(), + Deleted: true, + Txn: &txn.TxnMeta, + }).Size()) + mValSize += 2 + vKeySize := MVCCVersionTimestampSize // 12 + vValSize := int64(0) // tombstone + if disableSimpleValueEncoding { + vValSize += emptyMVCCValueHeaderSize // 7 + } + + expMS := enginepb.MVCCStats{ + LastUpdateNanos: 1e9, + LiveBytes: 0, + LiveCount: 0, + KeyBytes: mKeySize + vKeySize, + KeyCount: 1, + ValBytes: mValSize + vValSize, + ValCount: 1, + IntentCount: 1, + LockCount: 1, + IntentBytes: vKeySize + vValSize, + GCBytesAge: 0, + } + assertEq(t, engine, "after delete", aggMS, &expMS) + + txn.Sequence++ + + // Annoyingly, the new meta value is actually a little larger thanks to the + // sequence number. Also since there was a write previously on the same + // transaction, the IntentHistory will add a few bytes to the metadata. + encValue, err := EncodeMVCCValue(MVCCValue{Value: roachpb.Value{RawBytes: []byte{}}}) + require.NoError(t, err) + m2ValSize := int64((&enginepb.MVCCMetadata{ + Timestamp: ts.ToLegacyTimestamp(), + Txn: &txn.TxnMeta, + Deleted: false, + IntentHistory: []enginepb.MVCCMetadata_SequencedIntent{ + {Sequence: 0, Value: encValue}, + }, + }).Size()) + expM2ValSize := 54 + if disableSimpleValueEncoding { + expM2ValSize += int(emptyMVCCValueHeaderSize) + } + require.EqualValues(t, expM2ValSize, m2ValSize) + + // Put the value. + if _, err := MVCCPut(ctx, engine, key, ts, value, MVCCWriteOptions{Txn: txn, Stats: aggMS}); err != nil { + t.Fatal(err) + } + + v2ValSize := int64(len(value.RawBytes)) + if disableSimpleValueEncoding { + v2ValSize += emptyMVCCValueHeaderSize // 17 + } + + expAggMS := enginepb.MVCCStats{ + LastUpdateNanos: 1e9, + LiveBytes: mKeySize + m2ValSize + vKeySize + v2ValSize, + LiveCount: 1, + KeyCount: 1, + ValCount: 1, + KeyBytes: mKeySize + vKeySize, + ValBytes: m2ValSize + v2ValSize, + LockAge: 0, + IntentCount: 1, + LockCount: 1, + IntentBytes: vKeySize + v2ValSize, + GCBytesAge: 0, + } + + assertEq(t, engine, "after put", aggMS, &expAggMS) + + // Now commit the value and roll back the put. + txn.Status = roachpb.COMMITTED + txn.AddIgnoredSeqNumRange(enginepb.IgnoredSeqNumRange{Start: 1, End: 1}) + if _, _, _, _, err := MVCCResolveWriteIntent(ctx, engine, aggMS, + roachpb.MakeLockUpdate(txn, roachpb.Span{Key: key}), + MVCCResolveWriteIntentOptions{}, + ); err != nil { + t.Fatal(err) + } + + expAggMS = enginepb.MVCCStats{ + LastUpdateNanos: 1e9, + LiveBytes: 0, + LiveCount: 0, // the key is not live after the rollback + KeyCount: 1, + ValCount: 1, + KeyBytes: mKeySize + vKeySize, + ValBytes: vValSize, + GCBytesAge: 0, + } + + assertEq(t, engine, "after committing", aggMS, &expAggMS) +} + var mvccStatsTests = []struct { name string fn func(Reader, roachpb.Key, roachpb.Key, int64) (enginepb.MVCCStats, error) @@ -1596,6 +1851,9 @@ func (s *randomTest) step(t *testing.T) { actName := s.actionNames[s.rng.Intn(len(s.actionNames))] preTxn := s.Txn + if s.Txn != nil { + s.Txn.Sequence++ + } s.batch = s.internal.eng.NewBatch() *s.MSDelta = enginepb.MVCCStats{} ok, info := s.actions[actName](&s.state) @@ -1665,38 +1923,54 @@ func TestMVCCStatsRandomized(t *testing.T) { } actions["Put"] = func(s *state) (bool, string) { + ts := s.TS + if s.Txn != nil { + ts = s.Txn.ReadTimestamp + } opts := MVCCWriteOptions{ Txn: s.Txn, Stats: s.MSDelta, } - if _, err := MVCCPut(ctx, s.batch, s.key, s.TS, s.rngVal(), opts); err != nil { + if _, err := MVCCPut(ctx, s.batch, s.key, ts, s.rngVal(), opts); err != nil { return false, err.Error() } return true, "" } actions["InitPut"] = func(s *state) (bool, string) { + ts := s.TS + if s.Txn != nil { + ts = s.Txn.ReadTimestamp + } opts := MVCCWriteOptions{ Txn: s.Txn, Stats: s.MSDelta, } failOnTombstones := s.rng.Intn(2) == 0 desc := fmt.Sprintf("failOnTombstones=%t", failOnTombstones) - if _, err := MVCCInitPut(ctx, s.batch, s.key, s.TS, s.rngVal(), failOnTombstones, opts); err != nil { + if _, err := MVCCInitPut(ctx, s.batch, s.key, ts, s.rngVal(), failOnTombstones, opts); err != nil { return false, desc + ": " + err.Error() } return true, desc } actions["Del"] = func(s *state) (bool, string) { + ts := s.TS + if s.Txn != nil { + ts = s.Txn.ReadTimestamp + } opts := MVCCWriteOptions{ Txn: s.Txn, Stats: s.MSDelta, } - if _, _, err := MVCCDelete(ctx, s.batch, s.key, s.TS, opts); err != nil { + if _, _, err := MVCCDelete(ctx, s.batch, s.key, ts, opts); err != nil { return false, err.Error() } return true, "" } actions["DelRange"] = func(s *state) (bool, string) { + ts := s.TS + if s.Txn != nil { + ts = s.Txn.ReadTimestamp + } opts := MVCCWriteOptions{ Txn: s.Txn, Stats: s.MSDelta, @@ -1723,7 +1997,7 @@ func TestMVCCStatsRandomized(t *testing.T) { } if !s.inline && s.rng.Intn(2) == 0 { - predicates.StartTime.WallTime = s.rng.Int63n(s.TS.WallTime + 1) + predicates.StartTime.WallTime = s.rng.Int63n(ts.WallTime + 1) } } @@ -1738,7 +2012,7 @@ func TestMVCCStatsRandomized(t *testing.T) { if !mvccRangeDel { desc = fmt.Sprintf("mvccDeleteRange=%s, returnKeys=%t, max=%d", keySpan, returnKeys, max) _, _, _, _, err = MVCCDeleteRange( - ctx, s.batch, keySpan.Key, keySpan.EndKey, max, s.TS, opts, returnKeys, + ctx, s.batch, keySpan.Key, keySpan.EndKey, max, ts, opts, returnKeys, ) } else if predicates == (kvpb.DeleteRangePredicates{}) { desc = fmt.Sprintf("mvccDeleteRangeUsingTombstone=%s", @@ -1747,7 +2021,7 @@ func TestMVCCStatsRandomized(t *testing.T) { const maxLockConflicts = 0 // unlimited msCovered := (*enginepb.MVCCStats)(nil) err = MVCCDeleteRangeUsingTombstone( - ctx, s.batch, s.MSDelta, mvccRangeDelKey, mvccRangeDelEndKey, s.TS, hlc.ClockTimestamp{}, nil, /* leftPeekBound */ + ctx, s.batch, s.MSDelta, mvccRangeDelKey, mvccRangeDelEndKey, ts, hlc.ClockTimestamp{}, nil, /* leftPeekBound */ nil /* rightPeekBound */, idempotent, maxLockConflicts, msCovered, ) } else { @@ -1756,7 +2030,7 @@ func TestMVCCStatsRandomized(t *testing.T) { roachpb.Span{Key: mvccRangeDelKey, EndKey: mvccRangeDelEndKey}, predicates, rangeTombstoneThreshold) const maxLockConflicts = 0 // unlimited _, err = MVCCPredicateDeleteRange(ctx, s.batch, s.MSDelta, mvccRangeDelKey, mvccRangeDelEndKey, - s.TS, hlc.ClockTimestamp{}, nil /* leftPeekBound */, nil, /* rightPeekBound */ + ts, hlc.ClockTimestamp{}, nil /* leftPeekBound */, nil, /* rightPeekBound */ predicates, 0, 0, rangeTombstoneThreshold, maxLockConflicts) } if err != nil { @@ -1846,6 +2120,18 @@ func TestMVCCStatsRandomized(t *testing.T) { actions["Commit"] = func(s *state) (bool, string) { return resolve(s, roachpb.COMMITTED) } + actions["Rollback"] = func(s *state) (bool, string) { + if s.Txn != nil { + for i := 0; i < int(s.Txn.Sequence); i++ { + if s.rng.Intn(2) == 0 { + s.Txn.AddIgnoredSeqNumRange(enginepb.IgnoredSeqNumRange{Start: enginepb.TxnSeq(i), End: enginepb.TxnSeq(i)}) + } + } + desc := fmt.Sprintf("ignored=%v", s.Txn.IgnoredSeqNums) + return true, desc + } + return false, "" + } actions["Push"] = func(s *state) (bool, string) { return resolve(s, roachpb.PENDING) } diff --git a/pkg/storage/testdata/mvcc_histories/ignored_seq_nums_delete b/pkg/storage/testdata/mvcc_histories/ignored_seq_nums_delete new file mode 100644 index 000000000000..9ae0b88cda21 --- /dev/null +++ b/pkg/storage/testdata/mvcc_histories/ignored_seq_nums_delete @@ -0,0 +1,183 @@ +# When a write is rolled back and either the rolled back or preceding value is a +# delete, there are 3 cases to consider: +# 0. Rolled back put with a previous put (no actual delete; this is tested in +# ignored_seq_nums_commit). +# 1. Rolled back delete with a previous put. +# 2. Rolled back put with a preceding delete. +# 3. Rolled back delete with a preceding delete. +# We also test a case where the rolled back writes are surrounded by other puts. + +# Rolled back delete with a previous put. +run stats ok +with t=A + txn_begin ts=11 + txn_step seq=10 + put k=k v=a + txn_step seq=20 + del k=k + txn_step seq=30 + txn_ignore_seqs seqs=(19-21) + resolve_intent k=k +---- +>> put k=k v=a t=A +put: lock acquisition = {k id=00000001 key=/Min iso=Serializable pri=0.00000000 epo=0 ts=11.000000000,0 min=0,0 seq=10 Replicated Intent []} +stats: key_count=+1 key_bytes=+14 val_count=+1 val_bytes=+56 live_count=+1 live_bytes=+70 intent_count=+1 intent_bytes=+18 lock_count=+1 lock_age=+89 +>> del k=k t=A +del: "k": found key true +del: lock acquisition = {k id=00000001 key=/Min iso=Serializable pri=0.00000000 epo=0 ts=11.000000000,0 min=0,0 seq=20 Replicated Intent []} +stats: val_bytes=+4 live_count=-1 live_bytes=-70 gc_bytes_age=+6586 intent_bytes=-6 +>> resolve_intent k=k t=A +resolve_intent: "k" -> resolved key = true +stats: val_bytes=-54 live_count=+1 live_bytes=+20 gc_bytes_age=-6586 intent_count=-1 intent_bytes=-12 lock_count=-1 lock_age=-89 +>> at end: +txn: "A" meta={id=00000001 key=/Min iso=Serializable pri=0.00000000 epo=0 ts=11.000000000,0 min=0,0 seq=30} lock=true stat=PENDING rts=11.000000000,0 wto=false gul=0,0 isn=1 +data: "k"/11.000000000,0 -> /BYTES/a +stats: key_count=1 key_bytes=14 val_count=1 val_bytes=6 live_count=1 live_bytes=20 + +run ok +scan k=k end=-k +get k=k +---- +scan: "k"-"l" -> +get: "k" -> + +run ok +clear_range k=k end=-k +txn_remove t=A +---- +>> at end: + + +# Rolled back put with a preceding delete. +run stats ok +with t=A + txn_begin ts=11 + txn_step seq=10 + del k=k + txn_step seq=20 + put k=k v=a + txn_step seq=30 + txn_ignore_seqs seqs=(19-21) + resolve_intent k=k +---- +>> del k=k t=A +del: "k": found key false +del: lock acquisition = {k id=00000002 key=/Min iso=Serializable pri=0.00000000 epo=0 ts=11.000000000,0 min=0,0 seq=10 Replicated Intent []} +stats: key_count=+1 key_bytes=+14 val_count=+1 val_bytes=+50 gc_bytes_age=+5696 intent_count=+1 intent_bytes=+12 lock_count=+1 lock_age=+89 +>> put k=k v=a t=A +put: lock acquisition = {k id=00000002 key=/Min iso=Serializable pri=0.00000000 epo=0 ts=11.000000000,0 min=0,0 seq=20 Replicated Intent []} +stats: val_bytes=+10 live_count=+1 live_bytes=+74 gc_bytes_age=-5696 intent_bytes=+6 +>> resolve_intent k=k t=A +resolve_intent: "k" -> resolved key = true +stats: val_bytes=-60 live_count=-1 live_bytes=-74 gc_bytes_age=+1246 intent_count=-1 intent_bytes=-18 lock_count=-1 lock_age=-89 +>> at end: +txn: "A" meta={id=00000002 key=/Min iso=Serializable pri=0.00000000 epo=0 ts=11.000000000,0 min=0,0 seq=30} lock=true stat=PENDING rts=11.000000000,0 wto=false gul=0,0 isn=1 +data: "k"/11.000000000,0 -> / +stats: key_count=1 key_bytes=14 val_count=1 gc_bytes_age=1246 + +run ok +scan k=k end=-k +get k=k +---- +scan: "k"-"l" -> +get: "k" -> + +run ok +clear_range k=k end=-k +txn_remove t=A +---- +>> at end: + + +# Rolled back delete with a preceding delete. +run stats ok +with t=A + txn_begin ts=11 + txn_step seq=10 + del k=k + txn_step seq=20 + del k=k + txn_step seq=30 + txn_ignore_seqs seqs=(19-21) + resolve_intent k=k +---- +>> del k=k t=A +del: "k": found key false +del: lock acquisition = {k id=00000003 key=/Min iso=Serializable pri=0.00000000 epo=0 ts=11.000000000,0 min=0,0 seq=10 Replicated Intent []} +stats: key_count=+1 key_bytes=+14 val_count=+1 val_bytes=+50 gc_bytes_age=+5696 intent_count=+1 intent_bytes=+12 lock_count=+1 lock_age=+89 +>> del k=k t=A +del: "k": found key false +del: lock acquisition = {k id=00000003 key=/Min iso=Serializable pri=0.00000000 epo=0 ts=11.000000000,0 min=0,0 seq=20 Replicated Intent []} +stats: val_bytes=+4 gc_bytes_age=+356 +>> resolve_intent k=k t=A +resolve_intent: "k" -> resolved key = true +stats: val_bytes=-54 gc_bytes_age=-4806 intent_count=-1 intent_bytes=-12 lock_count=-1 lock_age=-89 +>> at end: +txn: "A" meta={id=00000003 key=/Min iso=Serializable pri=0.00000000 epo=0 ts=11.000000000,0 min=0,0 seq=30} lock=true stat=PENDING rts=11.000000000,0 wto=false gul=0,0 isn=1 +data: "k"/11.000000000,0 -> / +stats: key_count=1 key_bytes=14 val_count=1 gc_bytes_age=1246 + +run ok +scan k=k end=-k +get k=k +---- +scan: "k"-"l" -> +get: "k" -> + +run ok +clear_range k=k end=-k +txn_remove t=A +---- +>> at end: + + +# Rolled back delete and put with a previous values. +run stats ok +with t=A + txn_begin ts=11 + txn_step seq=10 + put k=k v=a + txn_step seq=20 + del k=k + txn_step seq=30 + put k=k v=b + txn_step seq=40 + put k=k v=c + txn_step seq=50 + txn_ignore_seqs seqs=(19-31) + resolve_intent k=k +---- +>> put k=k v=a t=A +put: lock acquisition = {k id=00000004 key=/Min iso=Serializable pri=0.00000000 epo=0 ts=11.000000000,0 min=0,0 seq=10 Replicated Intent []} +stats: key_count=+1 key_bytes=+14 val_count=+1 val_bytes=+56 live_count=+1 live_bytes=+70 intent_count=+1 intent_bytes=+18 lock_count=+1 lock_age=+89 +>> del k=k t=A +del: "k": found key true +del: lock acquisition = {k id=00000004 key=/Min iso=Serializable pri=0.00000000 epo=0 ts=11.000000000,0 min=0,0 seq=20 Replicated Intent []} +stats: val_bytes=+4 live_count=-1 live_bytes=-70 gc_bytes_age=+6586 intent_bytes=-6 +>> put k=k v=b t=A +put: lock acquisition = {k id=00000004 key=/Min iso=Serializable pri=0.00000000 epo=0 ts=11.000000000,0 min=0,0 seq=30 Replicated Intent []} +stats: val_bytes=+12 live_count=+1 live_bytes=+86 gc_bytes_age=-6586 intent_bytes=+6 +>> put k=k v=c t=A +put: lock acquisition = {k id=00000004 key=/Min iso=Serializable pri=0.00000000 epo=0 ts=11.000000000,0 min=0,0 seq=40 Replicated Intent []} +stats: val_bytes=+12 live_bytes=+12 +>> resolve_intent k=k t=A +resolve_intent: "k" -> resolved key = true +stats: val_bytes=-78 live_bytes=-78 intent_count=-1 intent_bytes=-18 lock_count=-1 lock_age=-89 +>> at end: +txn: "A" meta={id=00000004 key=/Min iso=Serializable pri=0.00000000 epo=0 ts=11.000000000,0 min=0,0 seq=50} lock=true stat=PENDING rts=11.000000000,0 wto=false gul=0,0 isn=1 +data: "k"/11.000000000,0 -> /BYTES/c +stats: key_count=1 key_bytes=14 val_count=1 val_bytes=6 live_count=1 live_bytes=20 + +run ok +scan k=k end=-k +get k=k +---- +scan: "k"-"l" -> +get: "k" -> + +run ok +clear_range k=k end=-k +txn_remove t=A +---- +>> at end: +