blob: 0a8b370c2abb5a23ce73955a3ba4692bdaab66f1 [file]
// Copyright 2023 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validate
import (
"errors"
"fmt"
"math"
"time"
"github.com/anishathalye/porcupine"
"go.uber.org/zap"
"go.etcd.io/etcd/tests/v3/robustness/model"
"go.etcd.io/etcd/tests/v3/robustness/report"
)
var ErrNotEmptyDatabase = errors.New("non empty database at start, required by model used for linearizability validation")
func ValidateAndReturnVisualize(lg *zap.Logger, cfg Config, reports []report.ClientReport, persistedRequests []model.EtcdRequest, timeout time.Duration) (result RobustnessResult) {
result.Assumptions = ResultFromError(checkValidationAssumptions(reports))
if result.Assumptions.Error() != nil {
return result
}
linearizableOperations, serializableOperations, operationsForVisualization := prepareAndCategorizeOperations(reports)
// We are passing in the original reports and linearizableOperations with modified return time.
// The reason is that linearizableOperations are those dedicated for linearization, which requires them to have returnTime set to infinity as required by pourcupine.
// As for the report, the original report is used so the consumer doesn't need to track what patching was done or not.
if len(persistedRequests) != 0 {
linearizableOperations = patchLinearizableOperations(linearizableOperations, reports, persistedRequests)
}
keys := model.ModelKeys(linearizableOperations)
result.Linearization = validateLinearizableOperationsAndVisualize(lg, keys, linearizableOperations, timeout)
result.Linearization.AddToVisualization(operationsForVisualization)
// Skip other validations if model is not linearizable, as they are expected to fail too and obfuscate the logs.
if result.Linearization.Error() != nil {
lg.Info("Skipping other validations as linearization failed")
return result
}
if len(persistedRequests) == 0 {
lg.Info("Skipping other validations as persisted requests were empty")
return result
}
replay := model.NewReplay(persistedRequests)
result.Watch = validateWatch(lg, cfg, reports, replay)
result.Serializable = validateSerializableOperations(lg, serializableOperations, replay)
return result
}
type Config struct {
ExpectRevisionUnique bool
}
func prepareAndCategorizeOperations(reports []report.ClientReport) (linearizable, serializable, forVisualization []porcupine.Operation) {
for _, report := range reports {
for _, op := range report.KeyValue {
request := op.Input.(model.EtcdRequest)
response := op.Output.(model.MaybeEtcdResponse)
if isSerializable(request, response) {
serializable = append(serializable, op)
}
// Operations that will not be linearized need to be added separately to the visualization.
if !isLinearizable(request, response) {
forVisualization = append(forVisualization, op)
continue
}
// For linearization, we set the return time of failed requests to MaxInt64.
// Failed requests can still be persisted, however we don't know when the request has taken effect.
if response.Error != "" {
op.Return = math.MaxInt64
}
linearizable = append(linearizable, op)
}
}
return linearizable, serializable, forVisualization
}
func isLinearizable(request model.EtcdRequest, response model.MaybeEtcdResponse) bool {
// Cannot test response for request without side effect.
if request.IsRead() && response.Error != "" {
return false
}
// Defragment is not linearizable
if request.Type == model.Defragment {
return false
}
return true
}
func isSerializable(request model.EtcdRequest, response model.MaybeEtcdResponse) bool {
// Cannot test response for request without side effect.
if request.IsRead() && response.Error != "" {
return false
}
// Test range requests about stale revision
if request.Type == model.Range && request.Range.Revision != 0 {
return true
}
return false
}
func checkValidationAssumptions(reports []report.ClientReport) error {
err := validateEmptyDatabaseAtStart(reports)
if err != nil {
return err
}
err = validateNonConcurrentClientRequests(reports)
if err != nil {
return err
}
return nil
}
func validateEmptyDatabaseAtStart(reports []report.ClientReport) error {
if len(reports) == 0 {
return nil
}
for _, r := range reports {
for _, op := range r.KeyValue {
request := op.Input.(model.EtcdRequest)
response := op.Output.(model.MaybeEtcdResponse)
if response.Revision == 1 && request.IsRead() {
return nil
}
}
}
return ErrNotEmptyDatabase
}
func validateNonConcurrentClientRequests(reports []report.ClientReport) error {
lastClientRequestReturn := map[int]int64{}
for _, r := range reports {
for _, op := range r.KeyValue {
lastRequest := lastClientRequestReturn[op.ClientId]
if op.Call <= lastRequest {
return fmt.Errorf("client %d has concurrent request, required for operation linearization", op.ClientId)
}
if op.Return <= op.Call {
return fmt.Errorf("operation %v ends before it starts, required for operation linearization", op)
}
lastClientRequestReturn[op.ClientId] = op.Return
}
}
return nil
}