Skip to content

Commit

Permalink
Add Version field to the robustness model
Browse files Browse the repository at this point in the history
It helps perform Txn before Compact in the same way
as it is done in Kubernetes.

Signed-off-by: Aleksander Mistewicz <[email protected]>
  • Loading branch information
AwesomePatrol committed Jan 21, 2025
1 parent c747ff5 commit db06a8b
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 52 deletions.
1 change: 1 addition & 0 deletions tests/robustness/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ func toWatchEvent(event clientv3.Event) (watch model.WatchEvent) {
watch.PrevValue = &model.ValueRevision{
Value: model.ToValueOrHash(string(event.PrevKv.Value)),
ModRevision: event.PrevKv.ModRevision,
Version: event.PrevKv.Version,
}
}
watch.IsCreate = event.IsCreate()
Expand Down
11 changes: 9 additions & 2 deletions tests/robustness/model/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ func describeGuaranteedTxn(txn *TxnRequest) string {
if txn.Conditions[0].Key != txn.OperationsOnSuccess[0].Put.Key || (len(txn.OperationsOnFailure) == 1 && txn.Conditions[0].Key != txn.OperationsOnFailure[0].Range.Start) {
return ""
}
if txn.Conditions[0].ExpectedVersion > 0 {
return ""
}
if txn.Conditions[0].ExpectedRevision == 0 {
return fmt.Sprintf("guaranteedCreate(%q, %s)", txn.Conditions[0].Key, describeValueOrHash(txn.OperationsOnSuccess[0].Put.Value))
}
Expand All @@ -106,8 +109,12 @@ func describeGuaranteedTxn(txn *TxnRequest) string {

func describeEtcdConditions(conds []EtcdCondition) string {
opsDescription := make([]string, len(conds))
for i := range conds {
opsDescription[i] = fmt.Sprintf("mod_rev(%s)==%d", conds[i].Key, conds[i].ExpectedRevision)
for i, cond := range conds {
if cond.ExpectedVersion > 0 {
opsDescription[i] = fmt.Sprintf("ver(%s)==%d", cond.Key, cond.ExpectedVersion)
} else {
opsDescription[i] = fmt.Sprintf("mod_rev(%s)==%d", cond.Key, cond.ExpectedRevision)
}
}
return strings.Join(opsDescription, " && ")
}
Expand Down
15 changes: 14 additions & 1 deletion tests/robustness/model/deterministic.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,13 @@ func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
case Txn:
failure := false
for _, cond := range request.Txn.Conditions {
if val := newState.KeyValues[cond.Key]; val.ModRevision != cond.ExpectedRevision {
val := newState.KeyValues[cond.Key]
if cond.ExpectedVersion > 0 {
if val.Version != cond.ExpectedVersion {
failure = true
break
}
} else if val.ModRevision != cond.ExpectedRevision {
failure = true
break
}
Expand All @@ -149,9 +155,14 @@ func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
if op.Put.LeaseID != 0 && !leaseExists {
break
}
ver := int64(1)
if val, exists := newState.KeyValues[op.Put.Key]; exists && val.Version > 0 {
ver = val.Version + 1
}
newState.KeyValues[op.Put.Key] = ValueRevision{
Value: op.Put.Value,
ModRevision: newState.Revision + 1,
Version: ver,
}
increaseRevision = true
newState = detachFromOldLease(newState, op.Put.Key)
Expand Down Expand Up @@ -326,6 +337,7 @@ type TxnRequest struct {
type EtcdCondition struct {
Key string
ExpectedRevision int64
ExpectedVersion int64
}

type EtcdOperation struct {
Expand Down Expand Up @@ -434,6 +446,7 @@ func (el EtcdLease) DeepCopy() EtcdLease {
type ValueRevision struct {
Value ValueOrHash
ModRevision int64
Version int64
}

type ValueOrHash struct {
Expand Down
60 changes: 30 additions & 30 deletions tests/robustness/model/deterministic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ var commonTestScenarios = []modelTestCase{
operations: []testOperation{
{req: putRequest("key1", "1"), resp: putResponse(2)},
{req: putRequest("key2", "2"), resp: putResponse(3)},
{req: listRequest("key", 0), resp: rangeResponse([]*mvccpb.KeyValue{{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2}, {Key: []byte("key2"), Value: []byte("2"), ModRevision: 3}}, 2, 3)},
{req: listRequest("key", 0), resp: rangeResponse([]*mvccpb.KeyValue{{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2}, {Key: []byte("key2"), Value: []byte("2"), ModRevision: 3}}, 2, 3)},
{req: listRequest("key", 0), resp: rangeResponse([]*mvccpb.KeyValue{{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2, Version: 1}, {Key: []byte("key2"), Value: []byte("2"), ModRevision: 3, Version: 1}}, 2, 3)},
{req: listRequest("key", 0), resp: rangeResponse([]*mvccpb.KeyValue{{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2, Version: 1}, {Key: []byte("key2"), Value: []byte("2"), ModRevision: 3, Version: 1}}, 2, 3)},
},
},
{
Expand All @@ -93,26 +93,26 @@ var commonTestScenarios = []modelTestCase{
{req: putRequest("key2", "2"), resp: putResponse(3)},
{req: putRequest("key3", "3"), resp: putResponse(4)},
{req: listRequest("key", 0), resp: rangeResponse([]*mvccpb.KeyValue{
{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2},
{Key: []byte("key2"), Value: []byte("2"), ModRevision: 3},
{Key: []byte("key3"), Value: []byte("3"), ModRevision: 4},
{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2, Version: 1},
{Key: []byte("key2"), Value: []byte("2"), ModRevision: 3, Version: 1},
{Key: []byte("key3"), Value: []byte("3"), ModRevision: 4, Version: 1},
}, 3, 4)},
{req: listRequest("key", 4), resp: rangeResponse([]*mvccpb.KeyValue{
{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2},
{Key: []byte("key2"), Value: []byte("2"), ModRevision: 3},
{Key: []byte("key3"), Value: []byte("3"), ModRevision: 4},
{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2, Version: 1},
{Key: []byte("key2"), Value: []byte("2"), ModRevision: 3, Version: 1},
{Key: []byte("key3"), Value: []byte("3"), ModRevision: 4, Version: 1},
}, 3, 4)},
{req: listRequest("key", 3), resp: rangeResponse([]*mvccpb.KeyValue{
{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2},
{Key: []byte("key2"), Value: []byte("2"), ModRevision: 3},
{Key: []byte("key3"), Value: []byte("3"), ModRevision: 4},
{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2, Version: 1},
{Key: []byte("key2"), Value: []byte("2"), ModRevision: 3, Version: 1},
{Key: []byte("key3"), Value: []byte("3"), ModRevision: 4, Version: 1},
}, 3, 4)},
{req: listRequest("key", 2), resp: rangeResponse([]*mvccpb.KeyValue{
{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2},
{Key: []byte("key2"), Value: []byte("2"), ModRevision: 3},
{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2, Version: 1},
{Key: []byte("key2"), Value: []byte("2"), ModRevision: 3, Version: 1},
}, 3, 4)},
{req: listRequest("key", 1), resp: rangeResponse([]*mvccpb.KeyValue{
{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2},
{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2, Version: 1},
}, 3, 4)},
},
},
Expand All @@ -123,19 +123,19 @@ var commonTestScenarios = []modelTestCase{
{req: putRequest("key2", "1"), resp: putResponse(3)},
{req: putRequest("key1", "2"), resp: putResponse(4)},
{req: listRequest("key", 0), resp: rangeResponse([]*mvccpb.KeyValue{
{Key: []byte("key1"), Value: []byte("2"), ModRevision: 4},
{Key: []byte("key2"), Value: []byte("1"), ModRevision: 3},
{Key: []byte("key3"), Value: []byte("3"), ModRevision: 2},
{Key: []byte("key1"), Value: []byte("2"), ModRevision: 4, Version: 1},
{Key: []byte("key2"), Value: []byte("1"), ModRevision: 3, Version: 1},
{Key: []byte("key3"), Value: []byte("3"), ModRevision: 2, Version: 1},
}, 3, 4)},
{req: listRequest("key", 0), resp: rangeResponse([]*mvccpb.KeyValue{
{Key: []byte("key2"), Value: []byte("1"), ModRevision: 3},
{Key: []byte("key1"), Value: []byte("2"), ModRevision: 4},
{Key: []byte("key3"), Value: []byte("3"), ModRevision: 2},
{Key: []byte("key2"), Value: []byte("1"), ModRevision: 3, Version: 1},
{Key: []byte("key1"), Value: []byte("2"), ModRevision: 4, Version: 1},
{Key: []byte("key3"), Value: []byte("3"), ModRevision: 2, Version: 1},
}, 3, 4), expectFailure: true},
{req: listRequest("key", 0), resp: rangeResponse([]*mvccpb.KeyValue{
{Key: []byte("key3"), Value: []byte("3"), ModRevision: 2},
{Key: []byte("key2"), Value: []byte("1"), ModRevision: 3},
{Key: []byte("key1"), Value: []byte("2"), ModRevision: 4},
{Key: []byte("key3"), Value: []byte("3"), ModRevision: 2, Version: 1},
{Key: []byte("key2"), Value: []byte("1"), ModRevision: 3, Version: 1},
{Key: []byte("key1"), Value: []byte("2"), ModRevision: 4, Version: 1},
}, 3, 4), expectFailure: true},
},
},
Expand All @@ -146,7 +146,7 @@ var commonTestScenarios = []modelTestCase{
{req: getRequest("key"), resp: getResponse("key", "123456789012345678901", 2, 2), expectFailure: true},
{req: getRequest("key"), resp: getResponse("key", "012345678901234567890", 2, 2)},
{req: putRequest("key", "123456789012345678901"), resp: putResponse(3)},
{req: getRequest("key"), resp: getResponse("key", "123456789012345678901", 3, 3)},
{req: getRequest("key"), resp: getResponseWithVer("key", "123456789012345678901", 2, 3, 3)},
{req: getRequest("key"), resp: getResponse("key", "012345678901234567890", 3, 3), expectFailure: true},
},
},
Expand Down Expand Up @@ -228,8 +228,8 @@ var commonTestScenarios = []modelTestCase{
{req: getRequest("key"), resp: getResponse("key", "1", 2, 2), expectFailure: true},
{req: getRequest("key"), resp: getResponse("key", "1", 2, 3), expectFailure: true},
{req: getRequest("key"), resp: getResponse("key", "1", 3, 3), expectFailure: true},
{req: getRequest("key"), resp: getResponse("key", "2", 2, 2), expectFailure: true},
{req: getRequest("key"), resp: getResponse("key", "2", 3, 3)},
{req: getRequest("key"), resp: getResponseWithVer("key", "2", 2, 2, 2), expectFailure: true},
{req: getRequest("key"), resp: getResponseWithVer("key", "2", 2, 3, 3)},
},
},
{
Expand All @@ -239,7 +239,7 @@ var commonTestScenarios = []modelTestCase{
{req: compareRevisionAndPutRequest("key1", 0, "2"), resp: compareRevisionAndPutResponse(true, 2)},
{req: compareRevisionAndPutRequest("key1", 0, "3"), resp: compareRevisionAndPutResponse(true, 3), expectFailure: true},
{req: txnRequestSingleOperation(compareRevision("key1", 0), putOperation("key1", "4"), putOperation("key1", "5")), resp: txnPutResponse(false, 3)},
{req: getRequest("key1"), resp: getResponse("key1", "5", 3, 3)},
{req: getRequest("key1"), resp: getResponseWithVer("key1", "5", 2, 3, 3)},
{req: compareRevisionAndPutRequest("key2", 0, "6"), resp: compareRevisionAndPutResponse(true, 4)},
},
},
Expand Down Expand Up @@ -291,7 +291,7 @@ var commonTestScenarios = []modelTestCase{
{req: putWithLeaseRequest("key", "2", 1), resp: putResponse(2)},
{req: putRequest("key", "3"), resp: putResponse(3)},
{req: leaseRevokeRequest(1), resp: leaseRevokeResponse(3)},
{req: getRequest("key"), resp: getResponse("key", "3", 3, 3)},
{req: getRequest("key"), resp: getResponseWithVer("key", "3", 2, 3, 3)},
},
},
{
Expand All @@ -302,7 +302,7 @@ var commonTestScenarios = []modelTestCase{
{req: putWithLeaseRequest("key", "2", 1), resp: putResponse(2)},
{req: putWithLeaseRequest("key", "3", 2), resp: putResponse(3)},
{req: leaseRevokeRequest(1), resp: leaseRevokeResponse(3)},
{req: getRequest("key"), resp: getResponse("key", "3", 3, 3)},
{req: getRequest("key"), resp: getResponseWithVer("key", "3", 2, 3, 3)},
{req: leaseRevokeRequest(2), resp: leaseRevokeResponse(4)},
{req: getRequest("key"), resp: emptyGetResponse(4)},
},
Expand All @@ -313,7 +313,7 @@ var commonTestScenarios = []modelTestCase{
{req: leaseGrantRequest(1), resp: leaseGrantResponse(1)},
{req: putWithLeaseRequest("key", "2", 1), resp: putResponse(2)},
{req: putWithLeaseRequest("key", "3", 1), resp: putResponse(3)},
{req: getRequest("key"), resp: getResponse("key", "3", 3, 3)},
{req: getRequest("key"), resp: getResponseWithVer("key", "3", 2, 3, 3)},
},
},
{
Expand Down
14 changes: 12 additions & 2 deletions tests/robustness/model/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,16 @@ func toEtcdCondition(cmp clientv3.Cmp) (cond EtcdCondition) {
switch {
case cmp.Result == etcdserverpb.Compare_EQUAL && cmp.Target == etcdserverpb.Compare_MOD:
cond.Key = string(cmp.KeyBytes())
cond.ExpectedRevision = cmp.TargetUnion.(*etcdserverpb.Compare_ModRevision).ModRevision
case cmp.Result == etcdserverpb.Compare_EQUAL && cmp.Target == etcdserverpb.Compare_CREATE:
cond.Key = string(cmp.KeyBytes())
cond.ExpectedRevision = cmp.TargetUnion.(*etcdserverpb.Compare_ModRevision).ModRevision
case cmp.Result == etcdserverpb.Compare_EQUAL && cmp.Target == etcdserverpb.Compare_VERSION:
cond.ExpectedVersion = cmp.TargetUnion.(*etcdserverpb.Compare_Version).Version
cond.Key = string(cmp.KeyBytes())
default:
panic(fmt.Sprintf("Compare not supported, target: %q, result: %q", cmp.Target, cmp.Result))
}
cond.ExpectedRevision = cmp.TargetUnion.(*etcdserverpb.Compare_ModRevision).ModRevision
return cond
}

Expand Down Expand Up @@ -228,6 +232,7 @@ func toEtcdOperationResult(resp *etcdserverpb.ResponseOp) EtcdOperationResult {
ValueRevision: ValueRevision{
Value: ToValueOrHash(string(kv.Value)),
ModRevision: kv.ModRevision,
Version: kv.Version,
},
}
}
Expand Down Expand Up @@ -342,7 +347,11 @@ func emptyGetResponse(revision int64) MaybeEtcdResponse {
}

func getResponse(key, value string, modRevision, revision int64) MaybeEtcdResponse {
return rangeResponse([]*mvccpb.KeyValue{{Key: []byte(key), Value: []byte(value), ModRevision: modRevision}}, 1, revision)
return getResponseWithVer(key, value, 1, modRevision, revision)
}

func getResponseWithVer(key, value string, ver, modRevision, revision int64) MaybeEtcdResponse {
return rangeResponse([]*mvccpb.KeyValue{{Key: []byte(key), Value: []byte(value), ModRevision: modRevision, Version: ver}}, 1, revision)
}

func rangeResponse(kvs []*mvccpb.KeyValue, count int64, revision int64) MaybeEtcdResponse {
Expand All @@ -354,6 +363,7 @@ func rangeResponse(kvs []*mvccpb.KeyValue, count int64, revision int64) MaybeEtc
ValueRevision: ValueRevision{
Value: ToValueOrHash(string(kv.Value)),
ModRevision: kv.ModRevision,
Version: kv.Version,
},
}
}
Expand Down
22 changes: 11 additions & 11 deletions tests/robustness/model/non_deterministic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ func TestModelNonDeterministic(t *testing.T) {
operations: []testOperation{
{req: putRequest("key1", "1"), resp: failedResponse(errors.New("failed"))},
{req: putRequest("key2", "2"), resp: putResponse(3)},
{req: listRequest("key", 0), resp: rangeResponse([]*mvccpb.KeyValue{{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2}, {Key: []byte("key2"), Value: []byte("2"), ModRevision: 3}}, 2, 3)},
{req: listRequest("key", 0), resp: rangeResponse([]*mvccpb.KeyValue{{Key: []byte("key1"), Value: []byte("1"), ModRevision: 2, Version: 1}, {Key: []byte("key2"), Value: []byte("2"), ModRevision: 3, Version: 1}}, 2, 3)},
},
},
{
name: "First Put request fails, and is lost",
operations: []testOperation{
{req: putRequest("key1", "1"), resp: failedResponse(errors.New("failed"))},
{req: putRequest("key2", "2"), resp: putResponse(2)},
{req: listRequest("key", 0), resp: rangeResponse([]*mvccpb.KeyValue{{Key: []byte("key2"), Value: []byte("2"), ModRevision: 2}}, 1, 2)},
{req: listRequest("key", 0), resp: rangeResponse([]*mvccpb.KeyValue{{Key: []byte("key2"), Value: []byte("2"), ModRevision: 2, Version: 1}}, 1, 2)},
},
},
{
Expand Down Expand Up @@ -90,14 +90,14 @@ func TestModelNonDeterministic(t *testing.T) {
// One failed request, one persisted.
{req: putRequest("key", "1"), resp: putResponse(2)},
{req: putRequest("key", "2"), resp: failedResponse(errors.New("failed"))},
{req: getRequest("key"), resp: getResponse("key", "3", 3, 3), expectFailure: true},
{req: getRequest("key"), resp: getResponse("key", "3", 2, 3), expectFailure: true},
{req: getRequest("key"), resp: getResponse("key", "2", 2, 2), expectFailure: true},
{req: getRequest("key"), resp: getResponse("key", "2", 3, 3)},
{req: getRequest("key"), resp: getResponseWithVer("key", "3", 2, 3, 3), expectFailure: true},
{req: getRequest("key"), resp: getResponseWithVer("key", "3", 2, 2, 3), expectFailure: true},
{req: getRequest("key"), resp: getResponseWithVer("key", "2", 2, 2, 2), expectFailure: true},
{req: getRequest("key"), resp: getResponseWithVer("key", "2", 2, 3, 3)},
// Two failed request, two persisted.
{req: putRequest("key", "3"), resp: failedResponse(errors.New("failed"))},
{req: putRequest("key", "4"), resp: failedResponse(errors.New("failed"))},
{req: getRequest("key"), resp: getResponse("key", "4", 5, 5)},
{req: getRequest("key"), resp: getResponseWithVer("key", "4", 4, 5, 5)},
},
},
{
Expand Down Expand Up @@ -133,7 +133,7 @@ func TestModelNonDeterministic(t *testing.T) {
{req: putRequest("key", "4"), resp: putResponse(4)},
{req: compareRevisionAndPutRequest("key", 5, ""), resp: compareRevisionAndPutResponse(false, 4)},
{req: putRequest("key", "5"), resp: failedResponse(errors.New("failed"))},
{req: getRequest("key"), resp: getResponse("key", "5", 5, 5)},
{req: getRequest("key"), resp: getResponseWithVer("key", "5", 4, 5, 5)},
},
},
{
Expand Down Expand Up @@ -249,13 +249,13 @@ func TestModelNonDeterministic(t *testing.T) {
// One failed request, one persisted.
{req: putRequest("key", "1"), resp: putResponse(2)},
{req: compareRevisionAndPutRequest("key", 2, "2"), resp: failedResponse(errors.New("failed"))},
{req: getRequest("key"), resp: getResponse("key", "2", 2, 2), expectFailure: true},
{req: getRequest("key"), resp: getResponse("key", "2", 3, 3)},
{req: getRequest("key"), resp: getResponseWithVer("key", "2", 2, 2, 2), expectFailure: true},
{req: getRequest("key"), resp: getResponseWithVer("key", "2", 2, 3, 3)},
// Two failed request, two persisted.
{req: putRequest("key", "3"), resp: putResponse(4)},
{req: compareRevisionAndPutRequest("key", 4, "4"), resp: failedResponse(errors.New("failed"))},
{req: compareRevisionAndPutRequest("key", 5, "5"), resp: failedResponse(errors.New("failed"))},
{req: getRequest("key"), resp: getResponse("key", "5", 6, 6)},
{req: getRequest("key"), resp: getResponseWithVer("key", "5", 5, 6, 6)},
},
},
{
Expand Down
7 changes: 7 additions & 0 deletions tests/robustness/report/wal.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ func parseEntryNormal(ent raftpb.Entry) (*model.EtcdRequest, error) {
OperationsOnFailure: []model.EtcdOperation{},
}
for _, cmp := range raftReq.Txn.Compare {
if ver := cmp.GetVersion(); ver > 0 {
txn.Conditions = append(txn.Conditions, model.EtcdCondition{
Key: string(cmp.Key),
ExpectedVersion: ver,
})
continue
}
txn.Conditions = append(txn.Conditions, model.EtcdCondition{
Key: string(cmp.Key),
ExpectedRevision: cmp.GetModRevision(),
Expand Down
6 changes: 2 additions & 4 deletions tests/robustness/traffic/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,8 @@ const (

func compact(ctx context.Context, client *client.RecordingClient, t, rev int64) (int64, int64, error) {
// Based on https://github.com/kubernetes/apiserver/blob/7dd4904f1896e11244ba3c5a59797697709de6b6/pkg/storage/etcd3/compact.go#L133-L162
// TODO: Use Version and not ModRevision when model supports key versioning.
resp, err := client.Txn(ctx).
If(clientv3.Compare(clientv3.ModRevision(compactRevKey), "=", t)).
If(clientv3.Compare(clientv3.Version(compactRevKey), "=", t)).
Then(clientv3.OpPut(compactRevKey, strconv.FormatInt(rev, 10))).
Else(clientv3.OpGet(compactRevKey)).
Commit()
Expand All @@ -247,8 +246,7 @@ func compact(ctx context.Context, client *client.RecordingClient, t, rev int64)
curRev := resp.Header.Revision

if !resp.Succeeded {
// TODO: Use Version and not ModRevision when model supports key versioning.
curTime := resp.Responses[0].GetResponseRange().Kvs[0].ModRevision
curTime := resp.Responses[0].GetResponseRange().Kvs[0].Version
return curTime, curRev, nil
}
curTime := t + 1
Expand Down
1 change: 1 addition & 0 deletions tests/robustness/validate/operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ func keyValueRevision(key, value string, rev int64) model.KeyValue {
ValueRevision: model.ValueRevision{
Value: model.ToValueOrHash(value),
ModRevision: rev,
Version: 1,
},
}
}
Loading

0 comments on commit db06a8b

Please sign in to comment.