github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/kv/kvnemesis/validator.go (about)

     1  // Copyright 2020 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package kvnemesis
    12  
    13  import (
    14  	"context"
    15  	"fmt"
    16  	"regexp"
    17  	"sort"
    18  	"strings"
    19  
    20  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    21  	"github.com/cockroachdb/cockroach/pkg/storage"
    22  	"github.com/cockroachdb/cockroach/pkg/util/hlc"
    23  	"github.com/cockroachdb/cockroach/pkg/util/uuid"
    24  	"github.com/cockroachdb/errors"
    25  	"github.com/cockroachdb/pebble"
    26  )
    27  
    28  // Validate checks for violations of our kv api guarantees. The Steps must all
    29  // have been applied and the kvs the result of those applications.
    30  //
    31  // For transactions, it is verified that all of their writes are present if and
    32  // only if the transaction committed (which is inferred from the KV data on
    33  // ambiguous results). Non-transactional read/write operations are treated as
    34  // though they had been wrapped in a transaction and are verified accordingly.
    35  //
    36  // TODO(dan): Verify that the results of reads match the data visible at the
    37  // commit timestamp. We should be able to construct a validity timespan for each
    38  // read in the transaction and fail if there isn't some timestamp that overlaps
    39  // all of them.
    40  //
    41  // TODO(dan): Verify that there is no causality inversion between steps. That
    42  // is, if transactions corresponding to two steps are sequential (i.e.
    43  // txn1CommitStep.After < txn2BeginStep.Before) then the commit timestamps need
    44  // to reflect this ordering.
    45  //
    46  // TODO(dan): Consider changing all of this validation to be based on the commit
    47  // timestamp as given back by kv.Txn. This doesn't currently work for
    48  // nontransactional read-only ops (i.e. single read or batch of only reads) but
    49  // that could be fixed by altering the API to communicating the timestamp back.
    50  //
    51  // Splits and merges are not verified for anything other than that they did not
    52  // return an error.
    53  func Validate(steps []Step, kvs *Engine) []error {
    54  	v, err := makeValidator(kvs)
    55  	if err != nil {
    56  		return []error{err}
    57  	}
    58  
    59  	// The validator works via AOST-style queries over the kvs emitted by
    60  	// RangeFeed. This means it can process steps in any order *except* that it
    61  	// needs to see all txn usage in order. Generator currently only emits
    62  	// ClosureTxnOperations, so it currently doesn't matter which order we process
    63  	// these.
    64  	//
    65  	// Originally there were separate operations for Begin/Use/Commit/Rollback
    66  	// Txn. If we add something like this back in (and I would like to), sorting
    67  	// by `After` timestamp is sufficient to get us the necessary ordering. This
    68  	// is because txns cannot be used concurrently, so none of the (Begin,After)
    69  	// timespans for a given transaction can overlap.
    70  	sort.Slice(steps, func(i, j int) bool { return steps[i].After.Less(steps[j].After) })
    71  	for _, s := range steps {
    72  		v.processOp(nil /* txnID */, s.Op)
    73  	}
    74  
    75  	var extraKVs []observedOp
    76  	for _, kv := range v.kvByValue {
    77  		kv := &observedWrite{
    78  			Key:          kv.Key.Key,
    79  			Value:        roachpb.Value{RawBytes: kv.Value},
    80  			Timestamp:    kv.Key.Timestamp,
    81  			Materialized: true,
    82  		}
    83  		extraKVs = append(extraKVs, kv)
    84  	}
    85  	if len(extraKVs) > 0 {
    86  		err := errors.Errorf(`extra writes: %s`, printObserved(extraKVs...))
    87  		v.failures = append(v.failures, err)
    88  	}
    89  
    90  	return v.failures
    91  }
    92  
    93  // timeSpan represents a range of time with an inclusive start and an exclusive
    94  // end.
    95  type timeSpan struct {
    96  	Start, End hlc.Timestamp
    97  }
    98  
    99  func (ts timeSpan) Intersect(o timeSpan) timeSpan {
   100  	i := ts
   101  	if i.Start.Less(o.Start) {
   102  		i.Start = o.Start
   103  	}
   104  	if o.End.Less(i.End) {
   105  		i.End = o.End
   106  	}
   107  	return i
   108  }
   109  
   110  func (ts timeSpan) IsEmpty() bool {
   111  	return !ts.Start.Less(ts.End)
   112  }
   113  
   114  // observedOp is the unification of an externally observed KV read or write.
   115  // Validator collects these grouped by txn, then computes the time window that
   116  // it would have been valid to observe this read or write, then asserts that all
   117  // operations in a transaction have time at which they all overlap. (For any
   118  // transaction containing a write, this will be a single time, but for read-only
   119  // transactions it will usually be a range of times.)
   120  type observedOp interface {
   121  	observedMarker()
   122  }
   123  
   124  type observedWrite struct {
   125  	Key   roachpb.Key
   126  	Value roachpb.Value
   127  	// Timestamp will only be filled if Materialized is true.
   128  	Timestamp    hlc.Timestamp
   129  	Materialized bool
   130  }
   131  
   132  func (*observedWrite) observedMarker() {}
   133  
   134  type observedRead struct {
   135  	Key   roachpb.Key
   136  	Value roachpb.Value
   137  	Valid timeSpan
   138  }
   139  
   140  func (*observedRead) observedMarker() {}
   141  
   142  type validator struct {
   143  	kvs              *Engine
   144  	observedOpsByTxn map[string][]observedOp
   145  
   146  	// NB: The Generator carefully ensures that each value written is unique
   147  	// globally over a run, so there's a 1:1 relationship between a value that was
   148  	// written and the operation that wrote it.
   149  	kvByValue map[string]storage.MVCCKeyValue
   150  
   151  	failures []error
   152  }
   153  
   154  func makeValidator(kvs *Engine) (*validator, error) {
   155  	kvByValue := make(map[string]storage.MVCCKeyValue)
   156  	var err error
   157  	kvs.Iterate(func(key storage.MVCCKey, value []byte, iterErr error) {
   158  		if iterErr != nil {
   159  			err = errors.CombineErrors(err, iterErr)
   160  			return
   161  		}
   162  		v := roachpb.Value{RawBytes: value}
   163  		if v.GetTag() != roachpb.ValueType_UNKNOWN {
   164  			valueStr := mustGetStringValue(value)
   165  			if existing, ok := kvByValue[valueStr]; ok {
   166  				// TODO(dan): This may be too strict. Some operations (db.Run on a
   167  				// Batch) seem to be double-committing. See #46374.
   168  				panic(errors.AssertionFailedf(
   169  					`invariant violation: value %s was written by two operations %s and %s`,
   170  					valueStr, existing.Key, key))
   171  			}
   172  			// NB: The Generator carefully ensures that each value written is unique
   173  			// globally over a run, so there's a 1:1 relationship between a value that
   174  			// was written and the operation that wrote it.
   175  			kvByValue[valueStr] = storage.MVCCKeyValue{Key: key, Value: value}
   176  		}
   177  	})
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  
   182  	return &validator{
   183  		kvs:              kvs,
   184  		kvByValue:        kvByValue,
   185  		observedOpsByTxn: make(map[string][]observedOp),
   186  	}, nil
   187  }
   188  
   189  func (v *validator) processOp(txnID *string, op Operation) {
   190  	switch t := op.GetValue().(type) {
   191  	case *GetOperation:
   192  		v.failIfError(op, t.Result)
   193  		if txnID == nil {
   194  			v.checkAtomic(`get`, t.Result, op)
   195  		} else {
   196  			read := &observedRead{
   197  				Key:   t.Key,
   198  				Value: roachpb.Value{RawBytes: t.Result.Value},
   199  			}
   200  			v.observedOpsByTxn[*txnID] = append(v.observedOpsByTxn[*txnID], read)
   201  		}
   202  	case *PutOperation:
   203  		if txnID == nil {
   204  			v.checkAtomic(`put`, t.Result, op)
   205  		} else {
   206  			// Accumulate all the writes for this transaction.
   207  			kv, ok := v.kvByValue[string(t.Value)]
   208  			delete(v.kvByValue, string(t.Value))
   209  			write := &observedWrite{
   210  				Key:          t.Key,
   211  				Value:        roachpb.MakeValueFromBytes(t.Value),
   212  				Materialized: ok,
   213  			}
   214  			if write.Materialized {
   215  				write.Timestamp = kv.Key.Timestamp
   216  			}
   217  			v.observedOpsByTxn[*txnID] = append(v.observedOpsByTxn[*txnID], write)
   218  		}
   219  	case *SplitOperation:
   220  		v.failIfError(op, t.Result)
   221  	case *MergeOperation:
   222  		if resultIsError(t.Result, `cannot merge final range`) {
   223  			// Because of some non-determinism, it is not worth it (or maybe not
   224  			// possible) to prevent these usage errors. Additionally, I (dan) think
   225  			// this hints at some unnecessary friction in the AdminMerge api. There is
   226  			// a similar inconsistency when a race condition means that AdminMerge is
   227  			// called on something that is not a split point. I propose that the
   228  			// AdminMerge contract should be that it can be called on any key, split
   229  			// point or not, and after a successful operation, the guarantee is that
   230  			// there is no split at that key. #44378
   231  			//
   232  			// In the meantime, no-op.
   233  		} else if resultIsError(t.Result, `merge failed: unexpected value`) {
   234  			// TODO(dan): If this error is going to remain a part of the kv API, we
   235  			// should make it sniffable with errors.As. Currently this seems to be
   236  			// broken by wrapping it with `roachpb.NewErrorf("merge failed: %s",
   237  			// err)`.
   238  			//
   239  			// However, I think the right thing to do is sniff this inside the
   240  			// AdminMerge code and retry so the client never sees it. In the meantime,
   241  			// no-op. #44377
   242  		} else if resultIsError(t.Result, `merge failed: cannot merge range with non-voter replicas`) {
   243  			// This operation executed concurrently with one that was changing
   244  			// replicas.
   245  		} else if resultIsError(t.Result, `merge failed: ranges not collocated`) {
   246  			// A merge requires that the two ranges have replicas on the same nodes,
   247  			// but Generator intentiontally does not try to avoid this so that this
   248  			// edge case is exercised.
   249  		} else if resultIsError(t.Result, `merge failed: waiting for all left-hand replicas to initialize`) {
   250  			// Probably should be transparently retried.
   251  		} else if resultIsError(t.Result, `merge failed: waiting for all right-hand replicas to catch up`) {
   252  			// Probably should be transparently retried.
   253  		} else if resultIsError(t.Result, `merge failed: non-deletion intent on local range descriptor`) {
   254  			// Probably should be transparently retried.
   255  		} else if resultIsError(t.Result, `merge failed: range missing intent on its local descriptor`) {
   256  			// Probably should be transparently retried.
   257  		} else {
   258  			v.failIfError(op, t.Result)
   259  		}
   260  	case *ChangeReplicasOperation:
   261  		if resultIsError(t.Result, `unable to add replica .* which is already present in`) {
   262  			// Generator created this operations based on data about a range's
   263  			// replicas that is now stale (because it raced with some other operation
   264  			// created by that Generator): a replica is being added and in the
   265  			// meantime, some other operation added the same replica.
   266  		} else if resultIsError(t.Result, `unable to add replica .* which is already present as a learner`) {
   267  			// Generator created this operations based on data about a range's
   268  			// replicas that is now stale (because it raced with some other operation
   269  			// created by that Generator): a replica is being added and in the
   270  			// meantime, some other operation started (but did not finish) adding the
   271  			// same replica.
   272  		} else if resultIsError(t.Result, `descriptor changed`) {
   273  			// Race between two operations being executed concurrently. Applier grabs
   274  			// a range descriptor and then calls AdminChangeReplicas with it, but the
   275  			// descriptor is changed by some other operation in between.
   276  		} else if resultIsError(t.Result, `received invalid ChangeReplicasTrigger .* to remove self \(leaseholder\)`) {
   277  			// Removing the leaseholder is invalid for technical reasons, but
   278  			// Generator intentiontally does not try to avoid this so that this edge
   279  			// case is exercised.
   280  		} else if resultIsError(t.Result, `removing .* which is not in`) {
   281  			// Generator created this operations based on data about a range's
   282  			// replicas that is now stale (because it raced with some other operation
   283  			// created by that Generator): a replica is being removed and in the
   284  			// meantime, some other operation removed the same replica.
   285  		} else if resultIsError(t.Result, `remote failed to apply snapshot for reason failed to apply snapshot: raft group deleted`) {
   286  			// Probably should be transparently retried.
   287  		} else if resultIsError(t.Result, `cannot apply snapshot: snapshot intersects existing range`) {
   288  			// Probably should be transparently retried.
   289  		} else if resultIsError(t.Result, `snapshot of type LEARNER was sent to .* which did not contain it as a replica`) {
   290  			// Probably should be transparently retried.
   291  		} else {
   292  			v.failIfError(op, t.Result)
   293  		}
   294  	case *BatchOperation:
   295  		if !resultIsRetryable(t.Result) {
   296  			v.failIfError(op, t.Result)
   297  			if txnID == nil {
   298  				v.checkAtomic(`batch`, t.Result, t.Ops...)
   299  			} else {
   300  				for _, op := range t.Ops {
   301  					v.processOp(txnID, op)
   302  				}
   303  			}
   304  		}
   305  	case *ClosureTxnOperation:
   306  		ops := t.Ops
   307  		if t.CommitInBatch != nil {
   308  			ops = append(ops, t.CommitInBatch.Ops...)
   309  		}
   310  		v.checkAtomic(`txn`, t.Result, ops...)
   311  	default:
   312  		panic(errors.AssertionFailedf(`unknown operation type: %T %v`, t, t))
   313  	}
   314  }
   315  
   316  func (v *validator) checkAtomic(atomicType string, result Result, ops ...Operation) {
   317  	fakeTxnID := uuid.MakeV4().String()
   318  	for _, op := range ops {
   319  		v.processOp(&fakeTxnID, op)
   320  	}
   321  	txnObservations := v.observedOpsByTxn[fakeTxnID]
   322  	delete(v.observedOpsByTxn, fakeTxnID)
   323  	if result.Type == ResultType_NoError {
   324  		v.checkCommittedTxn(`committed `+atomicType, txnObservations)
   325  	} else if resultIsAmbiguous(result) {
   326  		v.checkAmbiguousTxn(`ambiguous `+atomicType, txnObservations)
   327  	} else {
   328  		v.checkUncommittedTxn(`uncommitted `+atomicType, txnObservations)
   329  	}
   330  }
   331  
   332  func (v *validator) checkCommittedTxn(atomicType string, txnObservations []observedOp) {
   333  	// The following works by verifying that there is at least one time at which
   334  	// it was valid to see all the reads and writes that we saw in this
   335  	// transaction.
   336  	//
   337  	// Concretely a transaction:
   338  	// - Write k1@t2 -> v1
   339  	// - Read k2 -> v2
   340  	//
   341  	// And what was present in KV after this and some other transactions:
   342  	// - k1@t2, v1
   343  	// - k1@t3, v3
   344  	// - k2@t1, v2
   345  	// - k2@t3, v4
   346  	//
   347  	// Each of the operations in the transaction, if taken individually, has some
   348  	// window at which it was valid. The Write was only valid for a commit exactly
   349  	// at t2: [t2,t2). This is because each Write's mvcc timestamp is the
   350  	// timestamp of the txn commit. The Read would have been valid for [t1,t3)
   351  	// because v2 was written at t1 and overwritten at t3.
   352  	//
   353  	// As long as these time spans overlap, we're good. However, if another write
   354  	// had a timestamp of t3, then there is no timestamp at which the transaction
   355  	// could have committed, which is a violation of our consistency guarantees.
   356  	// Similarly if there was some read that was only valid from [t1,t2).
   357  	//
   358  	// Listen up, this is where it gets tricky. Within a transaction, if the same
   359  	// key is written more than once, only the last one will ever be materialized
   360  	// in KV (and be sent out over the RangeFeed). However, any reads in between
   361  	// will see the overwritten values. This means that each transaction needs its
   362  	// own view of KV when determining what is and is not valid.
   363  	//
   364  	// Concretely:
   365  	// - Read k -> <nil>
   366  	// - Write k -> v1
   367  	// - Read k -> v1
   368  	// - Write k -> v2
   369  	// - Read k -> v2
   370  	//
   371  	// This is okay, but if we only use the writes that come out of RangeFeed to
   372  	// compute our read validities, then there would be no time at which v1 could
   373  	// have been read. So, we have to "layer" the k -> v1 write on top of our
   374  	// RangeFeed output. At the same time, it would not have been valid for the
   375  	// first or second read to see v2 because when they ran that value hadn't
   376  	// been written yet.
   377  	//
   378  	// So, what we do to compute the read validity time windows is first hide all
   379  	// the writes the transaction eventually did in some "view". Then step through
   380  	// it, un-hiding each of them as we encounter each write, and using the
   381  	// current state of the view as we encounter each read. Luckily this is easy
   382  	// to do by with a pebble.Batch "view".
   383  	batch := v.kvs.kvs.NewIndexedBatch()
   384  	defer func() { _ = batch.Close() }()
   385  
   386  	// If the same key is written multiple times in a transaction, only the last
   387  	// one makes it to kv.
   388  	lastWriteIdxByKey := make(map[string]int, len(txnObservations))
   389  	for idx := len(txnObservations) - 1; idx >= 0; idx-- {
   390  		observation := txnObservations[idx]
   391  		switch o := observation.(type) {
   392  		case *observedWrite:
   393  			if _, ok := lastWriteIdxByKey[string(o.Key)]; !ok {
   394  				lastWriteIdxByKey[string(o.Key)] = idx
   395  			}
   396  			mvccKey := storage.MVCCKey{Key: o.Key, Timestamp: o.Timestamp}
   397  			if err := batch.Delete(storage.EncodeKey(mvccKey), nil); err != nil {
   398  				panic(err)
   399  			}
   400  		}
   401  	}
   402  
   403  	// Check if any key that was written twice in the txn had the overwritten
   404  	// writes materialize in kv. Also fill in all the read timestamps first so
   405  	// they show up in the failure message.
   406  	var failure string
   407  	for idx, observation := range txnObservations {
   408  		switch o := observation.(type) {
   409  		case *observedWrite:
   410  			if lastWriteIdx := lastWriteIdxByKey[string(o.Key)]; idx == lastWriteIdx {
   411  				// The last write of a given key in the txn wins and should have made it
   412  				// to kv.
   413  				mvccKey := storage.MVCCKey{Key: o.Key, Timestamp: o.Timestamp}
   414  				if err := batch.Set(storage.EncodeKey(mvccKey), o.Value.RawBytes, nil); err != nil {
   415  					panic(err)
   416  				}
   417  			} else {
   418  				if o.Materialized {
   419  					failure = `committed txn overwritten key had write`
   420  				}
   421  				// This write was never materialized in KV because the key got
   422  				// overwritten later in the txn. But reads in the txn could have seen
   423  				// it, so we put in the batch being maintained for validReadTime using
   424  				// the timestamp of the write for this key that eventually "won".
   425  				mvccKey := storage.MVCCKey{
   426  					Key:       o.Key,
   427  					Timestamp: txnObservations[lastWriteIdx].(*observedWrite).Timestamp,
   428  				}
   429  				if err := batch.Set(storage.EncodeKey(mvccKey), o.Value.RawBytes, nil); err != nil {
   430  					panic(err)
   431  				}
   432  			}
   433  		case *observedRead:
   434  			o.Valid = validReadTime(batch, o.Key, o.Value.RawBytes)
   435  		default:
   436  			panic(errors.AssertionFailedf(`unknown observedOp: %T %s`, observation, observation))
   437  		}
   438  	}
   439  
   440  	valid := timeSpan{Start: hlc.MinTimestamp, End: hlc.MaxTimestamp}
   441  	for idx, observation := range txnObservations {
   442  		if failure != `` {
   443  			break
   444  		}
   445  		var opValid timeSpan
   446  		switch o := observation.(type) {
   447  		case *observedWrite:
   448  			isLastWriteForKey := idx == lastWriteIdxByKey[string(o.Key)]
   449  			if !isLastWriteForKey {
   450  				continue
   451  			}
   452  			if !o.Materialized {
   453  				failure = atomicType + ` missing write`
   454  				continue
   455  			}
   456  			opValid = timeSpan{Start: o.Timestamp, End: o.Timestamp.Next()}
   457  		case *observedRead:
   458  			opValid = o.Valid
   459  		default:
   460  			panic(errors.AssertionFailedf(`unknown observedOp: %T %s`, observation, observation))
   461  		}
   462  		intersection := valid.Intersect(opValid)
   463  		if intersection.IsEmpty() {
   464  			failure = atomicType + ` non-atomic timestamps`
   465  		}
   466  		valid = intersection
   467  	}
   468  
   469  	if failure != `` {
   470  		err := errors.Errorf("%s: %s", failure, printObserved(txnObservations...))
   471  		v.failures = append(v.failures, err)
   472  	}
   473  }
   474  
   475  func (v *validator) checkAmbiguousTxn(atomicType string, txnObservations []observedOp) {
   476  	var somethingCommitted bool
   477  	var hadWrite bool
   478  	for _, observation := range txnObservations {
   479  		switch o := observation.(type) {
   480  		case *observedWrite:
   481  			hadWrite = true
   482  			if o.Materialized {
   483  				somethingCommitted = true
   484  				break
   485  			}
   486  		}
   487  	}
   488  	if !hadWrite {
   489  		// TODO(dan): Is it possible to receive an ambiguous read-only txn? Assume
   490  		// committed for now because the committed case has assertions about reads
   491  		// but the uncommitted case doesn't and this seems to work.
   492  		v.checkCommittedTxn(atomicType, txnObservations)
   493  	} else if somethingCommitted {
   494  		v.checkCommittedTxn(atomicType, txnObservations)
   495  	} else {
   496  		v.checkUncommittedTxn(atomicType, txnObservations)
   497  	}
   498  }
   499  
   500  func (v *validator) checkUncommittedTxn(atomicType string, txnObservations []observedOp) {
   501  	var failure string
   502  	for _, observed := range txnObservations {
   503  		if failure != `` {
   504  			break
   505  		}
   506  		switch o := observed.(type) {
   507  		case *observedWrite:
   508  			if o.Materialized {
   509  				failure = atomicType + ` had writes`
   510  			}
   511  		case *observedRead:
   512  			// TODO(dan): Figure out what we can assert about reads in an uncommitted
   513  			// transaction.
   514  		default:
   515  			panic(errors.AssertionFailedf(`unknown observedOp: %T %s`, observed, observed))
   516  		}
   517  	}
   518  
   519  	if failure != `` {
   520  		err := errors.Errorf("%s: %s", failure, printObserved(txnObservations...))
   521  		v.failures = append(v.failures, err)
   522  	}
   523  }
   524  
   525  func (v *validator) failIfError(op Operation, r Result) {
   526  	switch r.Type {
   527  	case ResultType_Unknown:
   528  		err := errors.AssertionFailedf(`unknown result %s`, op)
   529  		v.failures = append(v.failures, err)
   530  	case ResultType_Error:
   531  		ctx := context.Background()
   532  		err := errors.DecodeError(ctx, *r.Err)
   533  		err = errors.Wrapf(err, `error applying %s`, op)
   534  		v.failures = append(v.failures, err)
   535  	}
   536  }
   537  
   538  // TODO(dan): Checking errors using string containment is fragile at best and a
   539  // security issue at worst. Unfortunately, some errors that currently make it
   540  // out of our kv apis are created with `errors.New` and so do not have types
   541  // that can be sniffed. Some of these may be removed or handled differently but
   542  // the rest should graduate to documented parts of the public api. Remove this
   543  // once it happens.
   544  func resultIsError(r Result, msgRE string) bool {
   545  	if r.Type != ResultType_Error {
   546  		return false
   547  	}
   548  	ctx := context.Background()
   549  	err := errors.DecodeError(ctx, *r.Err)
   550  	return regexp.MustCompile(msgRE).MatchString(err.Error())
   551  }
   552  
   553  func resultIsRetryable(r Result) bool {
   554  	if r.Type != ResultType_Error {
   555  		return false
   556  	}
   557  	ctx := context.Background()
   558  	err := errors.DecodeError(ctx, *r.Err)
   559  	return errors.HasInterface(err, (*roachpb.ClientVisibleRetryError)(nil))
   560  }
   561  
   562  func resultIsAmbiguous(r Result) bool {
   563  	if r.Type != ResultType_Error {
   564  		return false
   565  	}
   566  	ctx := context.Background()
   567  	err := errors.DecodeError(ctx, *r.Err)
   568  	return errors.HasInterface(err, (*roachpb.ClientVisibleAmbiguousError)(nil))
   569  }
   570  
   571  func mustGetStringValue(value []byte) string {
   572  	if len(value) == 0 {
   573  		return `<nil>`
   574  	}
   575  	v, err := roachpb.Value{RawBytes: value}.GetBytes()
   576  	if err != nil {
   577  		panic(errors.Wrapf(err, "decoding %x", value))
   578  	}
   579  	return string(v)
   580  }
   581  
   582  func validReadTime(b *pebble.Batch, key roachpb.Key, value []byte) timeSpan {
   583  	var validTime []timeSpan
   584  	end := hlc.MaxTimestamp
   585  
   586  	iter := b.NewIter(nil)
   587  	defer func() { _ = iter.Close() }()
   588  	iter.SeekGE(storage.EncodeKey(storage.MVCCKey{Key: key}))
   589  	for ; iter.Valid(); iter.Next() {
   590  		mvccKey, err := storage.DecodeMVCCKey(iter.Key())
   591  		if err != nil {
   592  			panic(err)
   593  		}
   594  		if !mvccKey.Key.Equal(key) {
   595  			break
   596  		}
   597  		if mustGetStringValue(iter.Value()) == mustGetStringValue(value) {
   598  			validTime = append(validTime, timeSpan{Start: mvccKey.Timestamp, End: end})
   599  		}
   600  		end = mvccKey.Timestamp
   601  	}
   602  	if len(value) == 0 {
   603  		validTime = append(validTime, timeSpan{Start: hlc.MinTimestamp, End: end})
   604  	}
   605  
   606  	if len(validTime) == 0 {
   607  		return timeSpan{}
   608  	} else if len(validTime) == 1 {
   609  		return validTime[0]
   610  	} else {
   611  		// TODO(dan): Until we add deletes, the "only write each value once"
   612  		// property of the generator means that we have a 1:1 mapping between some
   613  		// `(key, possibly-nil-value)` observation and a time span in which it was
   614  		// valid. Once we add deletes, there will be multiple disjoint spans for the
   615  		// `(key, nil)` case.
   616  		panic(`unreachable`)
   617  	}
   618  }
   619  
   620  func printObserved(observedOps ...observedOp) string {
   621  	var buf strings.Builder
   622  	for _, observed := range observedOps {
   623  		if buf.Len() > 0 {
   624  			buf.WriteString(" ")
   625  		}
   626  		switch o := observed.(type) {
   627  		case *observedWrite:
   628  			ts := `missing`
   629  			if o.Materialized {
   630  				ts = o.Timestamp.String()
   631  			}
   632  			fmt.Fprintf(&buf, "[w]%s:%s->%s", o.Key, ts, mustGetStringValue(o.Value.RawBytes))
   633  		case *observedRead:
   634  			var start string
   635  			if o.Valid.Start == hlc.MinTimestamp {
   636  				start = `<min>`
   637  			} else {
   638  				start = o.Valid.Start.String()
   639  			}
   640  			var end string
   641  			if o.Valid.End == hlc.MaxTimestamp {
   642  				end = `<max>`
   643  			} else {
   644  				end = o.Valid.End.String()
   645  			}
   646  			fmt.Fprintf(&buf, "[r]%s:[%s,%s)->%s",
   647  				o.Key, start, end, mustGetStringValue(o.Value.RawBytes))
   648  		default:
   649  			panic(errors.AssertionFailedf(`unknown observedOp: %T %s`, observed, observed))
   650  		}
   651  	}
   652  	return buf.String()
   653  }