Skip to content

Commit 375808c

Browse files
committed
Use static list of keys in model
Signed-off-by: Marek Siarkowicz <[email protected]>
1 parent 592c195 commit 375808c

File tree

11 files changed

+443
-408
lines changed

11 files changed

+443
-408
lines changed

tests/robustness/model/describe_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ func TestModelDescribe(t *testing.T) {
171171
},
172172
}
173173
for _, tc := range tcs {
174-
assert.Equal(t, tc.expectDescribe, NonDeterministicModel.DescribeOperation(tc.req, tc.resp))
174+
m := NonDeterministicModelV2(nil)
175+
assert.Equal(t, tc.expectDescribe, m.DescribeOperation(tc.req, tc.resp))
175176
}
176177
}

tests/robustness/model/deterministic.go

Lines changed: 52 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -22,53 +22,42 @@ import (
2222
"html"
2323
"maps"
2424
"reflect"
25+
"slices"
2526
"sort"
2627

2728
"github.com/anishathalye/porcupine"
2829

2930
"go.etcd.io/etcd/server/v3/storage/mvcc"
3031
)
3132

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

6657
type EtcdState struct {
67-
Revision int64 `json:",omitempty"`
68-
CompactRevision int64 `json:",omitempty"`
69-
KeyValues map[string]ValueRevision `json:",omitempty"`
70-
KeyLeases map[string]int64 `json:",omitempty"`
71-
Leases map[int64]EtcdLease `json:",omitempty"`
58+
Revision int64 `json:",omitempty"`
59+
CompactRevision int64 `json:",omitempty"`
60+
Values []ValueRevision `json:",omitempty"`
7261
}
7362

7463
func (s EtcdState) Equal(other EtcdState) bool {
@@ -78,54 +67,45 @@ func (s EtcdState) Equal(other EtcdState) bool {
7867
if s.CompactRevision != other.CompactRevision {
7968
return false
8069
}
81-
if !reflect.DeepEqual(s.KeyValues, other.KeyValues) {
70+
if !slices.Equal(s.Values, other.Values) {
8271
return false
8372
}
84-
if !reflect.DeepEqual(s.KeyLeases, other.KeyLeases) {
85-
return false
86-
}
87-
return reflect.DeepEqual(s.Leases, other.Leases)
73+
return true
8874
}
8975

90-
func (s EtcdState) apply(request EtcdRequest, response EtcdResponse) (bool, EtcdState) {
91-
newState, modelResponse := s.Step(request)
76+
func (s EtcdState) apply(request EtcdRequest, keys []string, response EtcdResponse) (bool, EtcdState) {
77+
newState, modelResponse := s.Step(request, keys)
9278
return Match(MaybeEtcdResponse{EtcdResponse: response}, modelResponse), newState
9379
}
9480

9581
func (s EtcdState) DeepCopy() EtcdState {
9682
newState := EtcdState{
9783
Revision: s.Revision,
9884
CompactRevision: s.CompactRevision,
85+
Values: slices.Clone(s.Values),
9986
}
100-
101-
newState.KeyValues = maps.Clone(s.KeyValues)
102-
newState.KeyLeases = maps.Clone(s.KeyLeases)
103-
104-
newLeases := map[int64]EtcdLease{}
105-
for key, val := range s.Leases {
106-
newLeases[key] = val.DeepCopy()
107-
}
108-
newState.Leases = newLeases
10987
return newState
11088
}
11189

112-
func freshEtcdState() EtcdState {
90+
func freshEtcdState(size int) EtcdState {
11391
return EtcdState{
11492
Revision: 1,
11593
// Start from CompactRevision equal -1 as etcd allows client to compact revision 0 for some reason.
11694
CompactRevision: -1,
117-
KeyValues: map[string]ValueRevision{},
118-
KeyLeases: map[string]int64{},
119-
Leases: map[int64]EtcdLease{},
95+
Values: make([]ValueRevision, size),
12096
}
12197
}
12298

12399
// Step handles a successful request, returning updated state and response it would generate.
124-
func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
100+
func (s EtcdState) Step(request EtcdRequest, keys []string) (EtcdState, MaybeEtcdResponse) {
125101
// TODO: Avoid copying when TXN only has read operations
102+
kvs := keyValues{
103+
Keys: keys,
104+
Values: s.Values,
105+
}
126106
if request.Type == Range {
127107
if request.Range.Revision == 0 || request.Range.Revision == s.Revision {
128-
resp := s.getRange(request.Range.RangeOptions)
108+
resp := s.getRange(kvs, request.Range.RangeOptions)
129109
return s, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Range: &resp, Revision: s.Revision}}
130110
}
131111
if request.Range.Revision > s.Revision {
@@ -138,11 +118,12 @@ func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
138118
}
139119

140120
newState := s.DeepCopy()
121+
kvs.Values = newState.Values
141122
switch request.Type {
142123
case Txn:
143124
failure := false
144125
for _, cond := range request.Txn.Conditions {
145-
val := newState.KeyValues[cond.Key]
126+
val, _ := kvs.Get(cond.Key)
146127
if cond.ExpectedVersion > 0 {
147128
if val.Version != cond.ExpectedVersion {
148129
failure = true
@@ -163,32 +144,23 @@ func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
163144
switch op.Type {
164145
case RangeOperation:
165146
opResp[i] = EtcdOperationResult{
166-
RangeResponse: newState.getRange(op.Range),
147+
RangeResponse: newState.getRange(kvs, op.Range),
167148
}
168149
case PutOperation:
169-
_, leaseExists := newState.Leases[op.Put.LeaseID]
170-
if op.Put.LeaseID != 0 && !leaseExists {
171-
break
172-
}
173150
ver := int64(1)
174-
if val, exists := newState.KeyValues[op.Put.Key]; exists && val.Version > 0 {
151+
if val, exists := kvs.Get(op.Put.Key); exists && val.Version > 0 {
175152
ver = val.Version + 1
176153
}
177-
newState.KeyValues[op.Put.Key] = ValueRevision{
154+
kvs.Set(op.Put.Key, ValueRevision{
178155
Value: op.Put.Value,
179156
ModRevision: newState.Revision + 1,
180157
Version: ver,
181-
}
158+
})
182159
increaseRevision = true
183-
newState = detachFromOldLease(newState, op.Put.Key)
184-
if leaseExists {
185-
newState = attachToNewLease(newState, op.Put.LeaseID, op.Put.Key)
186-
}
187160
case DeleteOperation:
188-
if _, ok := newState.KeyValues[op.Delete.Key]; ok {
189-
delete(newState.KeyValues, op.Delete.Key)
161+
if _, ok := kvs.Get(op.Delete.Key); ok {
162+
kvs.Delete(op.Delete.Key)
190163
increaseRevision = true
191-
newState = detachFromOldLease(newState, op.Delete.Key)
192164
opResp[i].Deleted = 1
193165
}
194166
default:
@@ -198,33 +170,8 @@ func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
198170
if increaseRevision {
199171
newState.Revision++
200172
}
173+
newState.Values = kvs.Values
201174
return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Txn: &TxnResponse{Failure: failure, Results: opResp}, Revision: newState.Revision}}
202-
case LeaseGrant:
203-
lease := EtcdLease{
204-
LeaseID: request.LeaseGrant.LeaseID,
205-
Keys: map[string]struct{}{},
206-
}
207-
newState.Leases[request.LeaseGrant.LeaseID] = lease
208-
return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Revision: newState.Revision, LeaseGrant: &LeaseGrantReponse{}}}
209-
case LeaseRevoke:
210-
// Delete the keys attached to the lease
211-
keyDeleted := false
212-
for key := range newState.Leases[request.LeaseRevoke.LeaseID].Keys {
213-
// same as delete.
214-
if _, ok := newState.KeyValues[key]; ok {
215-
if !keyDeleted {
216-
keyDeleted = true
217-
}
218-
delete(newState.KeyValues, key)
219-
delete(newState.KeyLeases, key)
220-
}
221-
}
222-
// delete the lease
223-
delete(newState.Leases, request.LeaseRevoke.LeaseID)
224-
if keyDeleted {
225-
newState.Revision++
226-
}
227-
return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Revision: newState.Revision, LeaseRevoke: &LeaseRevokeResponse{}}}
228175
case Defragment:
229176
return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Defragment: &DefragmentResponse{}, Revision: newState.Revision}}
230177
case Compact:
@@ -240,18 +187,13 @@ func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
240187
}
241188
}
242189

243-
func (s EtcdState) getRange(options RangeOptions) RangeResponse {
190+
func (s EtcdState) getRange(kvs keyValues, options RangeOptions) RangeResponse {
244191
response := RangeResponse{
245192
KVs: []KeyValue{},
246193
}
247194
if options.End != "" {
248-
var count int64
249-
for k, v := range s.KeyValues {
250-
if k >= options.Start && k < options.End {
251-
response.KVs = append(response.KVs, KeyValue{Key: k, ValueRevision: v})
252-
count++
253-
}
254-
}
195+
response.KVs = kvs.Range(options.Start, options.End)
196+
count := int64(len(response.KVs))
255197
sort.Slice(response.KVs, func(j, k int) bool {
256198
return response.KVs[j].Key < response.KVs[k].Key
257199
})
@@ -260,7 +202,7 @@ func (s EtcdState) getRange(options RangeOptions) RangeResponse {
260202
}
261203
response.Count = count
262204
} else {
263-
value, ok := s.KeyValues[options.Start]
205+
value, ok := kvs.Get(options.Start)
264206
if ok {
265207
response.KVs = append(response.KVs, KeyValue{
266208
Key: options.Start,
@@ -272,20 +214,6 @@ func (s EtcdState) getRange(options RangeOptions) RangeResponse {
272214
return response
273215
}
274216

275-
func detachFromOldLease(s EtcdState, key string) EtcdState {
276-
if oldLeaseID, ok := s.KeyLeases[key]; ok {
277-
delete(s.Leases[oldLeaseID].Keys, key)
278-
delete(s.KeyLeases, key)
279-
}
280-
return s
281-
}
282-
283-
func attachToNewLease(s EtcdState, leaseID int64, key string) EtcdState {
284-
s.KeyLeases[key] = leaseID
285-
s.Leases[leaseID].Keys[key] = leased
286-
return s
287-
}
288-
289217
type RequestType string
290218

291219
const (

0 commit comments

Comments
 (0)