Skip to content

Use static list of keys in model #19973

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
3 changes: 2 additions & 1 deletion tests/robustness/model/describe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ func TestModelDescribe(t *testing.T) {
},
}
for _, tc := range tcs {
assert.Equal(t, tc.expectDescribe, NonDeterministicModel.DescribeOperation(tc.req, tc.resp))
m := NonDeterministicModelV2(nil)
assert.Equal(t, tc.expectDescribe, m.DescribeOperation(tc.req, tc.resp))
}
}
176 changes: 52 additions & 124 deletions tests/robustness/model/deterministic.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,53 +22,42 @@ import (
"html"
"maps"
"reflect"
"slices"
"sort"

"github.com/anishathalye/porcupine"

"go.etcd.io/etcd/server/v3/storage/mvcc"
)

// DeterministicModel assumes a deterministic execution of etcd requests. All
// requests that client called were executed and persisted by etcd. This
// assumption is good for simulating etcd behavior (aka writing a fake), but not
// for validating correctness as requests might be lost or interrupted. It
// requires perfect knowledge of what happened to request which is not possible
// in real systems.
//
// Model can still respond with error or partial response.
// - Error for etcd known errors, like future revision or compacted revision.
// - Incomplete response when requests is correct, but model doesn't have all
// to provide a full response. For example stale reads as model doesn't store
// whole change history as real etcd does.
var DeterministicModel = porcupine.Model{
Init: func() any {
return freshEtcdState()
},
Step: func(st any, in any, out any) (bool, any) {
return st.(EtcdState).apply(in.(EtcdRequest), out.(EtcdResponse))
},
Equal: func(st1, st2 any) bool {
return st1.(EtcdState).Equal(st2.(EtcdState))
},
DescribeOperation: func(in, out any) string {
return fmt.Sprintf("%s -> %s", describeEtcdRequest(in.(EtcdRequest)), describeEtcdResponse(in.(EtcdRequest), MaybeEtcdResponse{EtcdResponse: out.(EtcdResponse)}))
},
DescribeState: func(st any) string {
data, err := json.MarshalIndent(st, "", " ")
if err != nil {
panic(err)
}
return "<pre>" + html.EscapeString(string(data)) + "</pre>"
},
func DeterministicModelV2(keys []string) porcupine.Model {
return porcupine.Model{
Init: func() any {
return freshEtcdState(len(keys))
},
Step: func(st any, in any, out any) (bool, any) {
return st.(EtcdState).apply(in.(EtcdRequest), keys, out.(EtcdResponse))
},
Equal: func(st1, st2 any) bool {
return st1.(EtcdState).Equal(st2.(EtcdState))
},
DescribeOperation: func(in, out any) string {
return fmt.Sprintf("%s -> %s", describeEtcdRequest(in.(EtcdRequest)), describeEtcdResponse(in.(EtcdRequest), MaybeEtcdResponse{EtcdResponse: out.(EtcdResponse)}))
},
DescribeState: func(st any) string {
data, err := json.MarshalIndent(st, "", " ")
if err != nil {
panic(err)
}
return "<pre>" + html.EscapeString(string(data)) + "</pre>"
},
}
}

type EtcdState struct {
Revision int64 `json:",omitempty"`
CompactRevision int64 `json:",omitempty"`
KeyValues map[string]ValueRevision `json:",omitempty"`
KeyLeases map[string]int64 `json:",omitempty"`
Leases map[int64]EtcdLease `json:",omitempty"`
Revision int64 `json:",omitempty"`
CompactRevision int64 `json:",omitempty"`
Values []ValueRevision `json:",omitempty"`
}

func (s EtcdState) Equal(other EtcdState) bool {
Expand All @@ -78,54 +67,45 @@ func (s EtcdState) Equal(other EtcdState) bool {
if s.CompactRevision != other.CompactRevision {
return false
}
if !reflect.DeepEqual(s.KeyValues, other.KeyValues) {
if !slices.Equal(s.Values, other.Values) {
return false
}
if !reflect.DeepEqual(s.KeyLeases, other.KeyLeases) {
return false
}
return reflect.DeepEqual(s.Leases, other.Leases)
return true
}

func (s EtcdState) apply(request EtcdRequest, response EtcdResponse) (bool, EtcdState) {
newState, modelResponse := s.Step(request)
func (s EtcdState) apply(request EtcdRequest, keys []string, response EtcdResponse) (bool, EtcdState) {
newState, modelResponse := s.Step(request, keys)
return Match(MaybeEtcdResponse{EtcdResponse: response}, modelResponse), newState
}

func (s EtcdState) DeepCopy() EtcdState {
newState := EtcdState{
Revision: s.Revision,
CompactRevision: s.CompactRevision,
Values: slices.Clone(s.Values),
}

newState.KeyValues = maps.Clone(s.KeyValues)
newState.KeyLeases = maps.Clone(s.KeyLeases)

newLeases := map[int64]EtcdLease{}
for key, val := range s.Leases {
newLeases[key] = val.DeepCopy()
}
newState.Leases = newLeases
return newState
}

func freshEtcdState() EtcdState {
func freshEtcdState(size int) EtcdState {
return EtcdState{
Revision: 1,
// Start from CompactRevision equal -1 as etcd allows client to compact revision 0 for some reason.
CompactRevision: -1,
KeyValues: map[string]ValueRevision{},
KeyLeases: map[string]int64{},
Leases: map[int64]EtcdLease{},
Values: make([]ValueRevision, size),
}
}

// Step handles a successful request, returning updated state and response it would generate.
func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
func (s EtcdState) Step(request EtcdRequest, keys []string) (EtcdState, MaybeEtcdResponse) {
// TODO: Avoid copying when TXN only has read operations
kvs := keyValues{
Keys: keys,
Values: s.Values,
}
if request.Type == Range {
if request.Range.Revision == 0 || request.Range.Revision == s.Revision {
resp := s.getRange(request.Range.RangeOptions)
resp := s.getRange(kvs, request.Range.RangeOptions)
return s, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Range: &resp, Revision: s.Revision}}
}
if request.Range.Revision > s.Revision {
Expand All @@ -138,11 +118,12 @@ func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
}

newState := s.DeepCopy()
kvs.Values = newState.Values
switch request.Type {
case Txn:
failure := false
for _, cond := range request.Txn.Conditions {
val := newState.KeyValues[cond.Key]
val, _ := kvs.Get(cond.Key)
if cond.ExpectedVersion > 0 {
if val.Version != cond.ExpectedVersion {
failure = true
Expand All @@ -163,32 +144,23 @@ func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
switch op.Type {
case RangeOperation:
opResp[i] = EtcdOperationResult{
RangeResponse: newState.getRange(op.Range),
RangeResponse: newState.getRange(kvs, op.Range),
}
case PutOperation:
_, leaseExists := newState.Leases[op.Put.LeaseID]
if op.Put.LeaseID != 0 && !leaseExists {
break
}
ver := int64(1)
if val, exists := newState.KeyValues[op.Put.Key]; exists && val.Version > 0 {
if val, exists := kvs.Get(op.Put.Key); exists && val.Version > 0 {
ver = val.Version + 1
}
newState.KeyValues[op.Put.Key] = ValueRevision{
kvs.Set(op.Put.Key, ValueRevision{
Value: op.Put.Value,
ModRevision: newState.Revision + 1,
Version: ver,
}
})
increaseRevision = true
newState = detachFromOldLease(newState, op.Put.Key)
if leaseExists {
newState = attachToNewLease(newState, op.Put.LeaseID, op.Put.Key)
}
case DeleteOperation:
if _, ok := newState.KeyValues[op.Delete.Key]; ok {
delete(newState.KeyValues, op.Delete.Key)
if _, ok := kvs.Get(op.Delete.Key); ok {
kvs.Delete(op.Delete.Key)
increaseRevision = true
newState = detachFromOldLease(newState, op.Delete.Key)
opResp[i].Deleted = 1
}
default:
Expand All @@ -198,33 +170,8 @@ func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
if increaseRevision {
newState.Revision++
}
newState.Values = kvs.Values
return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Txn: &TxnResponse{Failure: failure, Results: opResp}, Revision: newState.Revision}}
case LeaseGrant:
lease := EtcdLease{
LeaseID: request.LeaseGrant.LeaseID,
Keys: map[string]struct{}{},
}
newState.Leases[request.LeaseGrant.LeaseID] = lease
return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Revision: newState.Revision, LeaseGrant: &LeaseGrantReponse{}}}
case LeaseRevoke:
// Delete the keys attached to the lease
keyDeleted := false
for key := range newState.Leases[request.LeaseRevoke.LeaseID].Keys {
// same as delete.
if _, ok := newState.KeyValues[key]; ok {
if !keyDeleted {
keyDeleted = true
}
delete(newState.KeyValues, key)
delete(newState.KeyLeases, key)
}
}
// delete the lease
delete(newState.Leases, request.LeaseRevoke.LeaseID)
if keyDeleted {
newState.Revision++
}
return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Revision: newState.Revision, LeaseRevoke: &LeaseRevokeResponse{}}}
case Defragment:
return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Defragment: &DefragmentResponse{}, Revision: newState.Revision}}
case Compact:
Expand All @@ -240,18 +187,13 @@ func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
}
}

func (s EtcdState) getRange(options RangeOptions) RangeResponse {
func (s EtcdState) getRange(kvs keyValues, options RangeOptions) RangeResponse {
response := RangeResponse{
KVs: []KeyValue{},
}
if options.End != "" {
var count int64
for k, v := range s.KeyValues {
if k >= options.Start && k < options.End {
response.KVs = append(response.KVs, KeyValue{Key: k, ValueRevision: v})
count++
}
}
response.KVs = kvs.Range(options.Start, options.End)
count := int64(len(response.KVs))
sort.Slice(response.KVs, func(j, k int) bool {
return response.KVs[j].Key < response.KVs[k].Key
})
Expand All @@ -260,7 +202,7 @@ func (s EtcdState) getRange(options RangeOptions) RangeResponse {
}
response.Count = count
} else {
value, ok := s.KeyValues[options.Start]
value, ok := kvs.Get(options.Start)
if ok {
response.KVs = append(response.KVs, KeyValue{
Key: options.Start,
Expand All @@ -272,20 +214,6 @@ func (s EtcdState) getRange(options RangeOptions) RangeResponse {
return response
}

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 RequestType string

const (
Expand Down
Loading