-
Notifications
You must be signed in to change notification settings - Fork 9.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
tests/robustness: Split model code into deterministic and non-determi…
…nistic Signed-off-by: Marek Siarkowicz <siarkowicz@google.com>
- Loading branch information
Showing
9 changed files
with
952 additions
and
505 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,304 @@ | ||
// 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 model | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"hash/fnv" | ||
"reflect" | ||
|
||
"github.com/anishathalye/porcupine" | ||
) | ||
|
||
// Base model implements a deterministic etcd model that assumes that all request succeed. | ||
var Base = porcupine.Model{ | ||
Init: func() interface{} { | ||
var s etcdState | ||
data, err := json.Marshal(s) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return string(data) | ||
}, | ||
Step: func(st interface{}, in interface{}, out interface{}) (bool, interface{}) { | ||
var s etcdState | ||
err := json.Unmarshal([]byte(st.(string)), &s) | ||
if err != nil { | ||
panic(err) | ||
} | ||
ok, s := s.Step(in.(EtcdRequest), out.(EtcdResponse)) | ||
data, err := json.Marshal(s) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return ok, string(data) | ||
}, | ||
DescribeOperation: func(in, out interface{}) string { | ||
return fmt.Sprintf("%s -> %s", describeEtcdRequest(in.(EtcdRequest)), describeEtcdResponse(in.(EtcdRequest), out.(EtcdResponse))) | ||
}, | ||
} | ||
|
||
type etcdState struct { | ||
Revision int64 | ||
KeyValues map[string]ValueOrHash | ||
KeyLeases map[string]int64 | ||
Leases map[int64]EtcdLease | ||
} | ||
|
||
func (s etcdState) Step(request EtcdRequest, response EtcdResponse) (bool, etcdState) { | ||
if s.Revision == 0 { | ||
return true, initState(request, response) | ||
} | ||
newState, gotResponse := s.step(request) | ||
return reflect.DeepEqual(response, gotResponse), newState | ||
} | ||
|
||
// initState tries to create etcd state based on the first request. | ||
func initState(request EtcdRequest, response EtcdResponse) etcdState { | ||
state := etcdState{ | ||
Revision: response.Revision, | ||
KeyValues: map[string]ValueOrHash{}, | ||
KeyLeases: map[string]int64{}, | ||
Leases: map[int64]EtcdLease{}, | ||
} | ||
switch request.Type { | ||
case Txn: | ||
if response.Txn.TxnResult { | ||
return state | ||
} | ||
for i, op := range request.Txn.Ops { | ||
opResp := response.Txn.OpsResult[i] | ||
switch op.Type { | ||
case Get: | ||
if opResp.Value.Value != "" && opResp.Value.Hash == 0 { | ||
state.KeyValues[op.Key] = opResp.Value | ||
} | ||
case Put: | ||
state.KeyValues[op.Key] = op.Value | ||
case Delete: | ||
default: | ||
panic("Unknown operation") | ||
} | ||
} | ||
case LeaseGrant: | ||
lease := EtcdLease{ | ||
LeaseID: request.LeaseGrant.LeaseID, | ||
Keys: map[string]struct{}{}, | ||
} | ||
state.Leases[request.LeaseGrant.LeaseID] = lease | ||
case LeaseRevoke: | ||
case Defragment: | ||
default: | ||
panic(fmt.Sprintf("Unknown request type: %v", request.Type)) | ||
} | ||
return state | ||
} | ||
|
||
// applyRequestToSingleState handles a successful request, returning updated state and response it would generate. | ||
func (s etcdState) step(request EtcdRequest) (etcdState, EtcdResponse) { | ||
newKVs := map[string]ValueOrHash{} | ||
for k, v := range s.KeyValues { | ||
newKVs[k] = v | ||
} | ||
s.KeyValues = newKVs | ||
switch request.Type { | ||
case Txn: | ||
success := true | ||
for _, cond := range request.Txn.Conds { | ||
if val := s.KeyValues[cond.Key]; val != cond.ExpectedValue { | ||
success = false | ||
break | ||
} | ||
} | ||
if !success { | ||
return s, EtcdResponse{Revision: s.Revision, Txn: &TxnResponse{TxnResult: true}} | ||
} | ||
opResp := make([]EtcdOperationResult, len(request.Txn.Ops)) | ||
increaseRevision := false | ||
for i, op := range request.Txn.Ops { | ||
switch op.Type { | ||
case Get: | ||
opResp[i].Value = s.KeyValues[op.Key] | ||
case Put: | ||
_, leaseExists := s.Leases[op.LeaseID] | ||
if op.LeaseID != 0 && !leaseExists { | ||
break | ||
} | ||
s.KeyValues[op.Key] = op.Value | ||
increaseRevision = true | ||
s = detachFromOldLease(s, op.Key) | ||
if leaseExists { | ||
s = attachToNewLease(s, op.LeaseID, op.Key) | ||
} | ||
case Delete: | ||
if _, ok := s.KeyValues[op.Key]; ok { | ||
delete(s.KeyValues, op.Key) | ||
increaseRevision = true | ||
s = detachFromOldLease(s, op.Key) | ||
opResp[i].Deleted = 1 | ||
} | ||
default: | ||
panic("unsupported operation") | ||
} | ||
} | ||
if increaseRevision { | ||
s.Revision += 1 | ||
} | ||
return s, EtcdResponse{Txn: &TxnResponse{OpsResult: opResp}, Revision: s.Revision} | ||
case LeaseGrant: | ||
lease := EtcdLease{ | ||
LeaseID: request.LeaseGrant.LeaseID, | ||
Keys: map[string]struct{}{}, | ||
} | ||
s.Leases[request.LeaseGrant.LeaseID] = lease | ||
return s, EtcdResponse{Revision: s.Revision, LeaseGrant: &LeaseGrantReponse{}} | ||
case LeaseRevoke: | ||
//Delete the keys attached to the lease | ||
keyDeleted := false | ||
for key := range s.Leases[request.LeaseRevoke.LeaseID].Keys { | ||
//same as delete. | ||
if _, ok := s.KeyValues[key]; ok { | ||
if !keyDeleted { | ||
keyDeleted = true | ||
} | ||
delete(s.KeyValues, key) | ||
delete(s.KeyLeases, key) | ||
} | ||
} | ||
//delete the lease | ||
delete(s.Leases, request.LeaseRevoke.LeaseID) | ||
if keyDeleted { | ||
s.Revision += 1 | ||
} | ||
return s, EtcdResponse{Revision: s.Revision, LeaseRevoke: &LeaseRevokeResponse{}} | ||
case Defragment: | ||
return s, EtcdResponse{Defragment: &DefragmentResponse{}, Revision: s.Revision} | ||
default: | ||
panic(fmt.Sprintf("Unknown request type: %v", request.Type)) | ||
} | ||
} | ||
|
||
func detachFromOldLease(s etcdState, key string) etcdState { | ||
if oldLeaseId, ok := s.KeyLeases[key]; ok { | ||
delete(s.Leases[oldLeaseId].Keys, key) | ||
delete(s.KeyLeases, key) | ||
} | ||
return s | ||
} | ||
|
||
func attachToNewLease(s etcdState, leaseID int64, key string) etcdState { | ||
s.KeyLeases[key] = leaseID | ||
s.Leases[leaseID].Keys[key] = leased | ||
return s | ||
} | ||
|
||
type OperationType string | ||
|
||
const ( | ||
Get OperationType = "get" | ||
Put OperationType = "put" | ||
Delete OperationType = "delete" | ||
) | ||
|
||
type RequestType string | ||
|
||
const ( | ||
Txn RequestType = "txn" | ||
LeaseGrant RequestType = "leaseGrant" | ||
LeaseRevoke RequestType = "leaseRevoke" | ||
Defragment RequestType = "defragment" | ||
) | ||
|
||
type EtcdRequest struct { | ||
Type RequestType | ||
LeaseGrant *LeaseGrantRequest | ||
LeaseRevoke *LeaseRevokeRequest | ||
Txn *TxnRequest | ||
Defragment *DefragmentRequest | ||
} | ||
|
||
type TxnRequest struct { | ||
Conds []EtcdCondition | ||
Ops []EtcdOperation | ||
} | ||
|
||
type EtcdCondition struct { | ||
Key string | ||
ExpectedValue ValueOrHash | ||
} | ||
|
||
type EtcdOperation struct { | ||
Type OperationType | ||
Key string | ||
Value ValueOrHash | ||
LeaseID int64 | ||
} | ||
|
||
type LeaseGrantRequest struct { | ||
LeaseID int64 | ||
} | ||
type LeaseRevokeRequest struct { | ||
LeaseID int64 | ||
} | ||
type DefragmentRequest struct{} | ||
|
||
type EtcdResponse struct { | ||
Revision int64 | ||
Txn *TxnResponse | ||
LeaseGrant *LeaseGrantReponse | ||
LeaseRevoke *LeaseRevokeResponse | ||
Defragment *DefragmentResponse | ||
} | ||
|
||
type TxnResponse struct { | ||
TxnResult bool | ||
OpsResult []EtcdOperationResult | ||
} | ||
|
||
type LeaseGrantReponse struct { | ||
LeaseID int64 | ||
} | ||
type LeaseRevokeResponse struct{} | ||
type DefragmentResponse struct{} | ||
|
||
type EtcdOperationResult struct { | ||
Value ValueOrHash | ||
Deleted int64 | ||
} | ||
|
||
var leased = struct{}{} | ||
|
||
type EtcdLease struct { | ||
LeaseID int64 | ||
Keys map[string]struct{} | ||
} | ||
|
||
type ValueOrHash struct { | ||
Value string | ||
Hash uint32 | ||
} | ||
|
||
func ToValueOrHash(value string) ValueOrHash { | ||
v := ValueOrHash{} | ||
if len(value) < 20 { | ||
v.Value = value | ||
} else { | ||
h := fnv.New32a() | ||
h.Write([]byte(value)) | ||
v.Hash = h.Sum32() | ||
} | ||
return v | ||
} |
Oops, something went wrong.