@@ -22,53 +22,42 @@ import (
22
22
"html"
23
23
"maps"
24
24
"reflect"
25
+ "slices"
25
26
"sort"
26
27
27
28
"github.com/anishathalye/porcupine"
28
29
29
30
"go.etcd.io/etcd/server/v3/storage/mvcc"
30
31
)
31
32
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
+ }
64
55
}
65
56
66
57
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"`
72
61
}
73
62
74
63
func (s EtcdState ) Equal (other EtcdState ) bool {
@@ -78,54 +67,45 @@ func (s EtcdState) Equal(other EtcdState) bool {
78
67
if s .CompactRevision != other .CompactRevision {
79
68
return false
80
69
}
81
- if ! reflect . DeepEqual (s .KeyValues , other .KeyValues ) {
70
+ if ! slices . Equal (s .Values , other .Values ) {
82
71
return false
83
72
}
84
- if ! reflect .DeepEqual (s .KeyLeases , other .KeyLeases ) {
85
- return false
86
- }
87
- return reflect .DeepEqual (s .Leases , other .Leases )
73
+ return true
88
74
}
89
75
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 )
92
78
return Match (MaybeEtcdResponse {EtcdResponse : response }, modelResponse ), newState
93
79
}
94
80
95
81
func (s EtcdState ) DeepCopy () EtcdState {
96
82
newState := EtcdState {
97
83
Revision : s .Revision ,
98
84
CompactRevision : s .CompactRevision ,
85
+ Values : slices .Clone (s .Values ),
99
86
}
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
109
87
return newState
110
88
}
111
89
112
- func freshEtcdState () EtcdState {
90
+ func freshEtcdState (size int ) EtcdState {
113
91
return EtcdState {
114
92
Revision : 1 ,
115
93
// Start from CompactRevision equal -1 as etcd allows client to compact revision 0 for some reason.
116
94
CompactRevision : - 1 ,
117
- KeyValues : map [string ]ValueRevision {},
118
- KeyLeases : map [string ]int64 {},
119
- Leases : map [int64 ]EtcdLease {},
95
+ Values : make ([]ValueRevision , size ),
120
96
}
121
97
}
122
98
123
99
// 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 ) {
125
101
// TODO: Avoid copying when TXN only has read operations
102
+ kvs := keyValues {
103
+ Keys : keys ,
104
+ Values : s .Values ,
105
+ }
126
106
if request .Type == Range {
127
107
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 )
129
109
return s , MaybeEtcdResponse {EtcdResponse : EtcdResponse {Range : & resp , Revision : s .Revision }}
130
110
}
131
111
if request .Range .Revision > s .Revision {
@@ -138,11 +118,12 @@ func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
138
118
}
139
119
140
120
newState := s .DeepCopy ()
121
+ kvs .Values = newState .Values
141
122
switch request .Type {
142
123
case Txn :
143
124
failure := false
144
125
for _ , cond := range request .Txn .Conditions {
145
- val := newState . KeyValues [ cond .Key ]
126
+ val , _ := kvs . Get ( cond .Key )
146
127
if cond .ExpectedVersion > 0 {
147
128
if val .Version != cond .ExpectedVersion {
148
129
failure = true
@@ -163,32 +144,23 @@ func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
163
144
switch op .Type {
164
145
case RangeOperation :
165
146
opResp [i ] = EtcdOperationResult {
166
- RangeResponse : newState .getRange (op .Range ),
147
+ RangeResponse : newState .getRange (kvs , op .Range ),
167
148
}
168
149
case PutOperation :
169
- _ , leaseExists := newState .Leases [op .Put .LeaseID ]
170
- if op .Put .LeaseID != 0 && ! leaseExists {
171
- break
172
- }
173
150
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 {
175
152
ver = val .Version + 1
176
153
}
177
- newState . KeyValues [ op .Put .Key ] = ValueRevision {
154
+ kvs . Set ( op .Put .Key , ValueRevision {
178
155
Value : op .Put .Value ,
179
156
ModRevision : newState .Revision + 1 ,
180
157
Version : ver ,
181
- }
158
+ })
182
159
increaseRevision = true
183
- newState = detachFromOldLease (newState , op .Put .Key )
184
- if leaseExists {
185
- newState = attachToNewLease (newState , op .Put .LeaseID , op .Put .Key )
186
- }
187
160
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 )
190
163
increaseRevision = true
191
- newState = detachFromOldLease (newState , op .Delete .Key )
192
164
opResp [i ].Deleted = 1
193
165
}
194
166
default :
@@ -198,33 +170,8 @@ func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
198
170
if increaseRevision {
199
171
newState .Revision ++
200
172
}
173
+ newState .Values = kvs .Values
201
174
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 {}}}
228
175
case Defragment :
229
176
return newState , MaybeEtcdResponse {EtcdResponse : EtcdResponse {Defragment : & DefragmentResponse {}, Revision : newState .Revision }}
230
177
case Compact :
@@ -240,18 +187,13 @@ func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
240
187
}
241
188
}
242
189
243
- func (s EtcdState ) getRange (options RangeOptions ) RangeResponse {
190
+ func (s EtcdState ) getRange (kvs keyValues , options RangeOptions ) RangeResponse {
244
191
response := RangeResponse {
245
192
KVs : []KeyValue {},
246
193
}
247
194
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 ))
255
197
sort .Slice (response .KVs , func (j , k int ) bool {
256
198
return response .KVs [j ].Key < response .KVs [k ].Key
257
199
})
@@ -260,7 +202,7 @@ func (s EtcdState) getRange(options RangeOptions) RangeResponse {
260
202
}
261
203
response .Count = count
262
204
} else {
263
- value , ok := s . KeyValues [ options .Start ]
205
+ value , ok := kvs . Get ( options .Start )
264
206
if ok {
265
207
response .KVs = append (response .KVs , KeyValue {
266
208
Key : options .Start ,
@@ -272,20 +214,6 @@ func (s EtcdState) getRange(options RangeOptions) RangeResponse {
272
214
return response
273
215
}
274
216
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
-
289
217
type RequestType string
290
218
291
219
const (
0 commit comments