Skip to content

Add non-linearizable requests to visualization #20194

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/robustness/validate/patch_history_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ func TestPatchHistory(t *testing.T) {
Watch: tc.watchOperations,
},
}
operations, _ := prepareAndCategorizeOperations(reports)
operations, _, _ := prepareAndCategorizeOperations(reports)
patched := patchLinearizableOperations(operations, reports, tc.persistedRequest)
if diff := cmp.Diff(tc.expectedRemainingOperations, patched,
cmpopts.EquateEmpty(),
Expand Down
15 changes: 14 additions & 1 deletion tests/robustness/validate/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,24 @@ type LinearizationResult struct {
Timeout bool
}

func (r LinearizationResult) Visualize(lg *zap.Logger, path string) error {
func (r *LinearizationResult) Visualize(lg *zap.Logger, path string) error {
lg.Info("Saving visualization", zap.String("path", path))
err := porcupine.VisualizePath(r.Model, r.Info, path)
if err != nil {
return fmt.Errorf("failed to visualize, err: %w", err)
}
return nil
}

func (r *LinearizationResult) AddToVisualization(serializable []porcupine.Operation) {
annotations := []porcupine.Annotation{}
for _, op := range serializable {
annotations = append(annotations, porcupine.Annotation{
ClientId: op.ClientId,
Start: op.Call,
End: op.Return,
Description: r.Model.DescribeOperation(op.Input, op.Output),
})
}
r.Info.AddAnnotations(annotations)
}
43 changes: 32 additions & 11 deletions tests/robustness/validate/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func ValidateAndReturnVisualize(lg *zap.Logger, cfg Config, reports []report.Cli
if result.Assumptions.Error() != nil {
return result
}
linearizableOperations, serializableOperations := prepareAndCategorizeOperations(reports)
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.
Expand All @@ -43,6 +43,7 @@ func ValidateAndReturnVisualize(lg *zap.Logger, cfg Config, reports []report.Cli
}

result.Linearization = validateLinearizableOperationsAndVisualize(lg, 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")
Expand All @@ -62,21 +63,17 @@ type Config struct {
ExpectRevisionUnique bool
}

func prepareAndCategorizeOperations(reports []report.ClientReport) (linearizable []porcupine.Operation, serializable []porcupine.Operation) {
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)
// serializable operations include only Range requests on non-zero revision
if request.Type == model.Range && request.Range.Revision != 0 {
if isSerializable(request, response) {
serializable = append(serializable, op)
}
// Remove failed read requests as they are not relevant for linearization.
if request.IsRead() && response.Error != "" {
continue
}
// Defragment is not linearizable
if request.Type == model.Defragment {
// Operation that will not be linearized need to be added separetly to visualization.
if !isLinearizable(request, response) {
forVisualization = append(forVisualization, op)
continue
}
// For linearization, we set the return time of failed requests to MaxInt64.
Expand All @@ -87,7 +84,31 @@ func prepareAndCategorizeOperations(reports []report.ClientReport) (linearizable
linearizable = append(linearizable, op)
}
}
return linearizable, serializable
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 {
Expand Down