mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
Merge pull request #15259 from serathius/linearizability-multi-op-txn
tests: Implement multi operation Txn
This commit is contained in:
commit
202d813c7b
@ -93,7 +93,20 @@ func (c *recordingClient) CompareAndSet(ctx context.Context, key, expectedValue,
|
|||||||
clientv3.OpPut(key, newValue),
|
clientv3.OpPut(key, newValue),
|
||||||
).Commit()
|
).Commit()
|
||||||
returnTime := time.Now()
|
returnTime := time.Now()
|
||||||
c.history.AppendTxn(key, expectedValue, newValue, callTime, returnTime, resp, err)
|
c.history.AppendCompareAndSet(key, expectedValue, newValue, callTime, returnTime, resp, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *recordingClient) Txn(ctx context.Context, cmp []clientv3.Cmp, ops []clientv3.Op) error {
|
||||||
|
callTime := time.Now()
|
||||||
|
txn := c.client.Txn(ctx)
|
||||||
|
resp, err := txn.If(
|
||||||
|
cmp...,
|
||||||
|
).Then(
|
||||||
|
ops...,
|
||||||
|
).Commit()
|
||||||
|
returnTime := time.Now()
|
||||||
|
c.history.AppendTxn(cmp, ops, callTime, returnTime, resp, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,13 +50,14 @@ var (
|
|||||||
maximalQPS: 200,
|
maximalQPS: 200,
|
||||||
clientCount: 8,
|
clientCount: 8,
|
||||||
traffic: traffic{
|
traffic: traffic{
|
||||||
keyCount: 4,
|
keyCount: 10,
|
||||||
leaseTTL: DefaultLeaseTTL,
|
leaseTTL: DefaultLeaseTTL,
|
||||||
largePutSize: 32769,
|
largePutSize: 32769,
|
||||||
writes: []requestChance{
|
writes: []requestChance{
|
||||||
{operation: Put, chance: 50},
|
{operation: Put, chance: 45},
|
||||||
{operation: LargePut, chance: 5},
|
{operation: LargePut, chance: 5},
|
||||||
{operation: Delete, chance: 10},
|
{operation: Delete, chance: 10},
|
||||||
|
{operation: MultiOpTxn, chance: 10},
|
||||||
{operation: PutWithLease, chance: 10},
|
{operation: PutWithLease, chance: 10},
|
||||||
{operation: LeaseRevoke, chance: 10},
|
{operation: LeaseRevoke, chance: 10},
|
||||||
{operation: CompareAndSet, chance: 10},
|
{operation: CompareAndSet, chance: 10},
|
||||||
@ -69,11 +70,12 @@ var (
|
|||||||
maximalQPS: 1000,
|
maximalQPS: 1000,
|
||||||
clientCount: 12,
|
clientCount: 12,
|
||||||
traffic: traffic{
|
traffic: traffic{
|
||||||
keyCount: 4,
|
keyCount: 10,
|
||||||
largePutSize: 32769,
|
largePutSize: 32769,
|
||||||
leaseTTL: DefaultLeaseTTL,
|
leaseTTL: DefaultLeaseTTL,
|
||||||
writes: []requestChance{
|
writes: []requestChance{
|
||||||
{operation: Put, chance: 90},
|
{operation: Put, chance: 85},
|
||||||
|
{operation: MultiOpTxn, chance: 10},
|
||||||
{operation: LargePut, chance: 5},
|
{operation: LargePut, chance: 5},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -15,10 +15,12 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/anishathalye/porcupine"
|
"github.com/anishathalye/porcupine"
|
||||||
|
|
||||||
|
"go.etcd.io/etcd/api/v3/etcdserverpb"
|
||||||
clientv3 "go.etcd.io/etcd/client/v3"
|
clientv3 "go.etcd.io/etcd/client/v3"
|
||||||
"go.etcd.io/etcd/tests/v3/linearizability/identity"
|
"go.etcd.io/etcd/tests/v3/linearizability/identity"
|
||||||
)
|
)
|
||||||
@ -161,8 +163,8 @@ func (h *AppendableHistory) AppendDelete(key string, start, end time.Time, resp
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AppendableHistory) AppendTxn(key, expectValue, newValue string, start, end time.Time, resp *clientv3.TxnResponse, err error) {
|
func (h *AppendableHistory) AppendCompareAndSet(key, expectValue, newValue string, start, end time.Time, resp *clientv3.TxnResponse, err error) {
|
||||||
request := txnRequest(key, expectValue, newValue)
|
request := compareAndSetRequest(key, expectValue, newValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.appendFailed(request, start, err)
|
h.appendFailed(request, start, err)
|
||||||
return
|
return
|
||||||
@ -175,11 +177,96 @@ func (h *AppendableHistory) AppendTxn(key, expectValue, newValue string, start,
|
|||||||
ClientId: h.id,
|
ClientId: h.id,
|
||||||
Input: request,
|
Input: request,
|
||||||
Call: start.UnixNano(),
|
Call: start.UnixNano(),
|
||||||
Output: txnResponse(resp.Succeeded, revision),
|
Output: compareAndSetResponse(resp.Succeeded, revision),
|
||||||
Return: end.UnixNano(),
|
Return: end.UnixNano(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *AppendableHistory) AppendTxn(cmp []clientv3.Cmp, onSuccess []clientv3.Op, start, end time.Time, resp *clientv3.TxnResponse, err error) {
|
||||||
|
conds := []EtcdCondition{}
|
||||||
|
for _, cmp := range cmp {
|
||||||
|
conds = append(conds, toEtcdCondition(cmp))
|
||||||
|
}
|
||||||
|
ops := []EtcdOperation{}
|
||||||
|
for _, op := range onSuccess {
|
||||||
|
ops = append(ops, toEtcdOperation(op))
|
||||||
|
}
|
||||||
|
request := txnRequest(conds, ops)
|
||||||
|
if err != nil {
|
||||||
|
h.appendFailed(request, start, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var revision int64
|
||||||
|
if resp != nil && resp.Header != nil {
|
||||||
|
revision = resp.Header.Revision
|
||||||
|
}
|
||||||
|
results := []EtcdOperationResult{}
|
||||||
|
for _, resp := range resp.Responses {
|
||||||
|
results = append(results, toEtcdOperationResult(resp))
|
||||||
|
}
|
||||||
|
h.successful = append(h.successful, porcupine.Operation{
|
||||||
|
ClientId: h.id,
|
||||||
|
Input: request,
|
||||||
|
Call: start.UnixNano(),
|
||||||
|
Output: txnResponse(results, resp.Succeeded, revision),
|
||||||
|
Return: end.UnixNano(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func toEtcdCondition(cmp clientv3.Cmp) (cond EtcdCondition) {
|
||||||
|
switch {
|
||||||
|
case cmp.Result == etcdserverpb.Compare_EQUAL && cmp.Target == etcdserverpb.Compare_VALUE:
|
||||||
|
cond.Key = string(cmp.KeyBytes())
|
||||||
|
cond.ExpectedValue = ToValueOrHash(string(cmp.ValueBytes()))
|
||||||
|
case cmp.Result == etcdserverpb.Compare_EQUAL && cmp.Target == etcdserverpb.Compare_CREATE:
|
||||||
|
cond.Key = string(cmp.KeyBytes())
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("Compare not supported, target: %q, result: %q", cmp.Target, cmp.Result))
|
||||||
|
}
|
||||||
|
return cond
|
||||||
|
}
|
||||||
|
|
||||||
|
func toEtcdOperation(op clientv3.Op) EtcdOperation {
|
||||||
|
var opType OperationType
|
||||||
|
switch {
|
||||||
|
case op.IsGet():
|
||||||
|
opType = Get
|
||||||
|
case op.IsPut():
|
||||||
|
opType = Put
|
||||||
|
case op.IsDelete():
|
||||||
|
opType = Delete
|
||||||
|
default:
|
||||||
|
panic("Unsupported operation")
|
||||||
|
}
|
||||||
|
return EtcdOperation{
|
||||||
|
Type: opType,
|
||||||
|
Key: string(op.KeyBytes()),
|
||||||
|
Value: ValueOrHash{Value: string(op.ValueBytes())},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toEtcdOperationResult(resp *etcdserverpb.ResponseOp) EtcdOperationResult {
|
||||||
|
switch {
|
||||||
|
case resp.GetResponseRange() != nil:
|
||||||
|
getResp := resp.GetResponseRange()
|
||||||
|
var val string
|
||||||
|
if len(getResp.Kvs) != 0 {
|
||||||
|
val = string(getResp.Kvs[0].Value)
|
||||||
|
}
|
||||||
|
return EtcdOperationResult{
|
||||||
|
Value: ToValueOrHash(val),
|
||||||
|
}
|
||||||
|
case resp.GetResponsePut() != nil:
|
||||||
|
return EtcdOperationResult{}
|
||||||
|
case resp.GetResponseDeleteRange() != nil:
|
||||||
|
return EtcdOperationResult{
|
||||||
|
Deleted: resp.GetResponseDeleteRange().Deleted,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("Unsupported operation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *AppendableHistory) AppendDefragment(start, end time.Time, resp *clientv3.DefragmentResponse, err error) {
|
func (h *AppendableHistory) AppendDefragment(start, end time.Time, resp *clientv3.DefragmentResponse, err error) {
|
||||||
request := defragmentRequest()
|
request := defragmentRequest()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -240,15 +327,23 @@ func deleteResponse(deleted int64, revision int64) EtcdResponse {
|
|||||||
return EtcdResponse{Txn: &TxnResponse{OpsResult: []EtcdOperationResult{{Deleted: deleted}}}, Revision: revision}
|
return EtcdResponse{Txn: &TxnResponse{OpsResult: []EtcdOperationResult{{Deleted: deleted}}}, Revision: revision}
|
||||||
}
|
}
|
||||||
|
|
||||||
func txnRequest(key, expectValue, newValue string) EtcdRequest {
|
func compareAndSetRequest(key, expectValue, newValue string) EtcdRequest {
|
||||||
return EtcdRequest{Type: Txn, Txn: &TxnRequest{Conds: []EtcdCondition{{Key: key, ExpectedValue: ToValueOrHash(expectValue)}}, Ops: []EtcdOperation{{Type: Put, Key: key, Value: ToValueOrHash(newValue)}}}}
|
return txnRequest([]EtcdCondition{{Key: key, ExpectedValue: ToValueOrHash(expectValue)}}, []EtcdOperation{{Type: Put, Key: key, Value: ToValueOrHash(newValue)}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func txnResponse(succeeded bool, revision int64) EtcdResponse {
|
func compareAndSetResponse(succeeded bool, revision int64) EtcdResponse {
|
||||||
var result []EtcdOperationResult
|
var result []EtcdOperationResult
|
||||||
if succeeded {
|
if succeeded {
|
||||||
result = []EtcdOperationResult{{}}
|
result = []EtcdOperationResult{{}}
|
||||||
}
|
}
|
||||||
|
return txnResponse(result, succeeded, revision)
|
||||||
|
}
|
||||||
|
|
||||||
|
func txnRequest(conds []EtcdCondition, onSuccess []EtcdOperation) EtcdRequest {
|
||||||
|
return EtcdRequest{Type: Txn, Txn: &TxnRequest{Conds: conds, Ops: onSuccess}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func txnResponse(result []EtcdOperationResult, succeeded bool, revision int64) EtcdResponse {
|
||||||
return EtcdResponse{Txn: &TxnResponse{OpsResult: result, TxnResult: !succeeded}, Revision: revision}
|
return EtcdResponse{Txn: &TxnResponse{OpsResult: result, TxnResult: !succeeded}, Revision: revision}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ func TestModelStep(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "First Txn can start from non-zero revision",
|
name: "First Txn can start from non-zero revision",
|
||||||
operations: []testOperation{
|
operations: []testOperation{
|
||||||
{req: txnRequest("key", "", "42"), resp: txnResponse(false, 42)},
|
{req: compareAndSetRequest("key", "", "42"), resp: compareAndSetResponse(false, 42)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -118,11 +118,11 @@ func TestModelStep(t *testing.T) {
|
|||||||
// Txn failure
|
// Txn failure
|
||||||
{req: getRequest("key"), resp: getResponse("", 1)},
|
{req: getRequest("key"), resp: getResponse("", 1)},
|
||||||
{req: putRequest("key", "1"), resp: failedResponse(errors.New("failed"))},
|
{req: putRequest("key", "1"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: txnRequest("key", "2", "3"), resp: txnResponse(false, 1)},
|
{req: compareAndSetRequest("key", "2", "3"), resp: compareAndSetResponse(false, 1)},
|
||||||
// Txn success
|
// Txn success
|
||||||
{req: putRequest("key", "2"), resp: putResponse(2)},
|
{req: putRequest("key", "2"), resp: putResponse(2)},
|
||||||
{req: putRequest("key", "4"), resp: failedResponse(errors.New("failed"))},
|
{req: putRequest("key", "4"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: txnRequest("key", "2", "5"), resp: txnResponse(true, 3)},
|
{req: compareAndSetRequest("key", "2", "5"), resp: compareAndSetResponse(true, 3)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -171,11 +171,11 @@ func TestModelStep(t *testing.T) {
|
|||||||
// Txn success
|
// Txn success
|
||||||
{req: getRequest("key"), resp: getResponse("", 1)},
|
{req: getRequest("key"), resp: getResponse("", 1)},
|
||||||
{req: putRequest("key", "2"), resp: failedResponse(errors.New("failed"))},
|
{req: putRequest("key", "2"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: txnRequest("key", "2", ""), resp: txnResponse(true, 2), failure: true},
|
{req: compareAndSetRequest("key", "2", ""), resp: compareAndSetResponse(true, 2), failure: true},
|
||||||
{req: txnRequest("key", "2", ""), resp: txnResponse(true, 3)},
|
{req: compareAndSetRequest("key", "2", ""), resp: compareAndSetResponse(true, 3)},
|
||||||
// Txn failure
|
// Txn failure
|
||||||
{req: putRequest("key", "4"), resp: putResponse(4)},
|
{req: putRequest("key", "4"), resp: putResponse(4)},
|
||||||
{req: txnRequest("key", "5", ""), resp: txnResponse(false, 4)},
|
{req: compareAndSetRequest("key", "5", ""), resp: compareAndSetResponse(false, 4)},
|
||||||
{req: putRequest("key", "5"), resp: failedResponse(errors.New("failed"))},
|
{req: putRequest("key", "5"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: getRequest("key"), resp: getResponse("5", 5)},
|
{req: getRequest("key"), resp: getResponse("5", 5)},
|
||||||
},
|
},
|
||||||
@ -282,21 +282,21 @@ func TestModelStep(t *testing.T) {
|
|||||||
// Txn success
|
// Txn success
|
||||||
{req: getRequest("key"), resp: getResponse("1", 1)},
|
{req: getRequest("key"), resp: getResponse("1", 1)},
|
||||||
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
|
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: txnRequest("key", "", "3"), resp: txnResponse(true, 3)},
|
{req: compareAndSetRequest("key", "", "3"), resp: compareAndSetResponse(true, 3)},
|
||||||
// Txn failure
|
// Txn failure
|
||||||
{req: putRequest("key", "4"), resp: putResponse(4)},
|
{req: putRequest("key", "4"), resp: putResponse(4)},
|
||||||
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
|
{req: deleteRequest("key"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: txnRequest("key", "4", "5"), resp: txnResponse(false, 5)},
|
{req: compareAndSetRequest("key", "4", "5"), resp: compareAndSetResponse(false, 5)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Txn sets new value if value matches expected",
|
name: "Txn sets new value if value matches expected",
|
||||||
operations: []testOperation{
|
operations: []testOperation{
|
||||||
{req: getRequest("key"), resp: getResponse("1", 1)},
|
{req: getRequest("key"), resp: getResponse("1", 1)},
|
||||||
{req: txnRequest("key", "1", "2"), resp: txnResponse(true, 1), failure: true},
|
{req: compareAndSetRequest("key", "1", "2"), resp: compareAndSetResponse(true, 1), failure: true},
|
||||||
{req: txnRequest("key", "1", "2"), resp: txnResponse(false, 2), failure: true},
|
{req: compareAndSetRequest("key", "1", "2"), resp: compareAndSetResponse(false, 2), failure: true},
|
||||||
{req: txnRequest("key", "1", "2"), resp: txnResponse(false, 1), failure: true},
|
{req: compareAndSetRequest("key", "1", "2"), resp: compareAndSetResponse(false, 1), failure: true},
|
||||||
{req: txnRequest("key", "1", "2"), resp: txnResponse(true, 2)},
|
{req: compareAndSetRequest("key", "1", "2"), resp: compareAndSetResponse(true, 2)},
|
||||||
{req: getRequest("key"), resp: getResponse("1", 1), failure: true},
|
{req: getRequest("key"), resp: getResponse("1", 1), failure: true},
|
||||||
{req: getRequest("key"), resp: getResponse("1", 2), failure: true},
|
{req: getRequest("key"), resp: getResponse("1", 2), failure: true},
|
||||||
{req: getRequest("key"), resp: getResponse("2", 1), failure: true},
|
{req: getRequest("key"), resp: getResponse("2", 1), failure: true},
|
||||||
@ -307,19 +307,19 @@ func TestModelStep(t *testing.T) {
|
|||||||
name: "Txn can expect on empty key",
|
name: "Txn can expect on empty key",
|
||||||
operations: []testOperation{
|
operations: []testOperation{
|
||||||
{req: getRequest("key1"), resp: getResponse("", 1)},
|
{req: getRequest("key1"), resp: getResponse("", 1)},
|
||||||
{req: txnRequest("key1", "", "2"), resp: txnResponse(true, 2)},
|
{req: compareAndSetRequest("key1", "", "2"), resp: compareAndSetResponse(true, 2)},
|
||||||
{req: txnRequest("key2", "", "3"), resp: txnResponse(true, 3)},
|
{req: compareAndSetRequest("key2", "", "3"), resp: compareAndSetResponse(true, 3)},
|
||||||
{req: txnRequest("key3", "4", "4"), resp: txnResponse(false, 4), failure: true},
|
{req: compareAndSetRequest("key3", "4", "4"), resp: compareAndSetResponse(false, 4), failure: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Txn doesn't do anything if value doesn't match expected",
|
name: "Txn doesn't do anything if value doesn't match expected",
|
||||||
operations: []testOperation{
|
operations: []testOperation{
|
||||||
{req: getRequest("key"), resp: getResponse("1", 1)},
|
{req: getRequest("key"), resp: getResponse("1", 1)},
|
||||||
{req: txnRequest("key", "2", "3"), resp: txnResponse(true, 2), failure: true},
|
{req: compareAndSetRequest("key", "2", "3"), resp: compareAndSetResponse(true, 2), failure: true},
|
||||||
{req: txnRequest("key", "2", "3"), resp: txnResponse(true, 1), failure: true},
|
{req: compareAndSetRequest("key", "2", "3"), resp: compareAndSetResponse(true, 1), failure: true},
|
||||||
{req: txnRequest("key", "2", "3"), resp: txnResponse(false, 2), failure: true},
|
{req: compareAndSetRequest("key", "2", "3"), resp: compareAndSetResponse(false, 2), failure: true},
|
||||||
{req: txnRequest("key", "2", "3"), resp: txnResponse(false, 1)},
|
{req: compareAndSetRequest("key", "2", "3"), resp: compareAndSetResponse(false, 1)},
|
||||||
{req: getRequest("key"), resp: getResponse("2", 1), failure: true},
|
{req: getRequest("key"), resp: getResponse("2", 1), failure: true},
|
||||||
{req: getRequest("key"), resp: getResponse("2", 2), failure: true},
|
{req: getRequest("key"), resp: getResponse("2", 2), failure: true},
|
||||||
{req: getRequest("key"), resp: getResponse("3", 1), failure: true},
|
{req: getRequest("key"), resp: getResponse("3", 1), failure: true},
|
||||||
@ -331,7 +331,7 @@ func TestModelStep(t *testing.T) {
|
|||||||
name: "Txn can fail and be lost before get",
|
name: "Txn can fail and be lost before get",
|
||||||
operations: []testOperation{
|
operations: []testOperation{
|
||||||
{req: getRequest("key"), resp: getResponse("1", 1)},
|
{req: getRequest("key"), resp: getResponse("1", 1)},
|
||||||
{req: txnRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: getRequest("key"), resp: getResponse("1", 1)},
|
{req: getRequest("key"), resp: getResponse("1", 1)},
|
||||||
{req: getRequest("key"), resp: getResponse("2", 2), failure: true},
|
{req: getRequest("key"), resp: getResponse("2", 2), failure: true},
|
||||||
},
|
},
|
||||||
@ -340,7 +340,7 @@ func TestModelStep(t *testing.T) {
|
|||||||
name: "Txn can fail and be lost before delete",
|
name: "Txn can fail and be lost before delete",
|
||||||
operations: []testOperation{
|
operations: []testOperation{
|
||||||
{req: getRequest("key"), resp: getResponse("1", 1)},
|
{req: getRequest("key"), resp: getResponse("1", 1)},
|
||||||
{req: txnRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: deleteRequest("key"), resp: deleteResponse(1, 2)},
|
{req: deleteRequest("key"), resp: deleteResponse(1, 2)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -348,7 +348,7 @@ func TestModelStep(t *testing.T) {
|
|||||||
name: "Txn can fail and be lost before put",
|
name: "Txn can fail and be lost before put",
|
||||||
operations: []testOperation{
|
operations: []testOperation{
|
||||||
{req: getRequest("key"), resp: getResponse("1", 1)},
|
{req: getRequest("key"), resp: getResponse("1", 1)},
|
||||||
{req: txnRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: putRequest("key", "3"), resp: putResponse(2)},
|
{req: putRequest("key", "3"), resp: putResponse(2)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -357,13 +357,13 @@ func TestModelStep(t *testing.T) {
|
|||||||
operations: []testOperation{
|
operations: []testOperation{
|
||||||
// One failed request, one persisted.
|
// One failed request, one persisted.
|
||||||
{req: getRequest("key"), resp: getResponse("1", 1)},
|
{req: getRequest("key"), resp: getResponse("1", 1)},
|
||||||
{req: txnRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: getRequest("key"), resp: getResponse("2", 1), failure: true},
|
{req: getRequest("key"), resp: getResponse("2", 1), failure: true},
|
||||||
{req: getRequest("key"), resp: getResponse("2", 2)},
|
{req: getRequest("key"), resp: getResponse("2", 2)},
|
||||||
// Two failed request, two persisted.
|
// Two failed request, two persisted.
|
||||||
{req: putRequest("key", "3"), resp: putResponse(3)},
|
{req: putRequest("key", "3"), resp: putResponse(3)},
|
||||||
{req: txnRequest("key", "3", "4"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "3", "4"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: txnRequest("key", "4", "5"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "4", "5"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: getRequest("key"), resp: getResponse("5", 5)},
|
{req: getRequest("key"), resp: getResponse("5", 5)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -372,12 +372,12 @@ func TestModelStep(t *testing.T) {
|
|||||||
operations: []testOperation{
|
operations: []testOperation{
|
||||||
// One failed request, one persisted.
|
// One failed request, one persisted.
|
||||||
{req: getRequest("key"), resp: getResponse("1", 1)},
|
{req: getRequest("key"), resp: getResponse("1", 1)},
|
||||||
{req: txnRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: putRequest("key", "3"), resp: putResponse(3)},
|
{req: putRequest("key", "3"), resp: putResponse(3)},
|
||||||
// Two failed request, two persisted.
|
// Two failed request, two persisted.
|
||||||
{req: putRequest("key", "4"), resp: putResponse(4)},
|
{req: putRequest("key", "4"), resp: putResponse(4)},
|
||||||
{req: txnRequest("key", "4", "5"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "4", "5"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: txnRequest("key", "5", "6"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "5", "6"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: putRequest("key", "7"), resp: putResponse(7)},
|
{req: putRequest("key", "7"), resp: putResponse(7)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -386,12 +386,12 @@ func TestModelStep(t *testing.T) {
|
|||||||
operations: []testOperation{
|
operations: []testOperation{
|
||||||
// One failed request, one persisted.
|
// One failed request, one persisted.
|
||||||
{req: getRequest("key"), resp: getResponse("1", 1)},
|
{req: getRequest("key"), resp: getResponse("1", 1)},
|
||||||
{req: txnRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: deleteRequest("key"), resp: deleteResponse(1, 3)},
|
{req: deleteRequest("key"), resp: deleteResponse(1, 3)},
|
||||||
// Two failed request, two persisted.
|
// Two failed request, two persisted.
|
||||||
{req: putRequest("key", "4"), resp: putResponse(4)},
|
{req: putRequest("key", "4"), resp: putResponse(4)},
|
||||||
{req: txnRequest("key", "4", "5"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "4", "5"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: txnRequest("key", "5", "6"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "5", "6"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: deleteRequest("key"), resp: deleteResponse(1, 7)},
|
{req: deleteRequest("key"), resp: deleteResponse(1, 7)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -400,17 +400,17 @@ func TestModelStep(t *testing.T) {
|
|||||||
operations: []testOperation{
|
operations: []testOperation{
|
||||||
// One failed request, one persisted with success.
|
// One failed request, one persisted with success.
|
||||||
{req: getRequest("key"), resp: getResponse("1", 1)},
|
{req: getRequest("key"), resp: getResponse("1", 1)},
|
||||||
{req: txnRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "1", "2"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: txnRequest("key", "2", "3"), resp: txnResponse(true, 3)},
|
{req: compareAndSetRequest("key", "2", "3"), resp: compareAndSetResponse(true, 3)},
|
||||||
// Two failed request, two persisted with success.
|
// Two failed request, two persisted with success.
|
||||||
{req: putRequest("key", "4"), resp: putResponse(4)},
|
{req: putRequest("key", "4"), resp: putResponse(4)},
|
||||||
{req: txnRequest("key", "4", "5"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "4", "5"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: txnRequest("key", "5", "6"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "5", "6"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: txnRequest("key", "6", "7"), resp: txnResponse(true, 7)},
|
{req: compareAndSetRequest("key", "6", "7"), resp: compareAndSetResponse(true, 7)},
|
||||||
// One failed request, one persisted with failure.
|
// One failed request, one persisted with failure.
|
||||||
{req: putRequest("key", "8"), resp: putResponse(8)},
|
{req: putRequest("key", "8"), resp: putResponse(8)},
|
||||||
{req: txnRequest("key", "8", "9"), resp: failedResponse(errors.New("failed"))},
|
{req: compareAndSetRequest("key", "8", "9"), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: txnRequest("key", "8", "10"), resp: txnResponse(false, 9)},
|
{req: compareAndSetRequest("key", "8", "10"), resp: compareAndSetResponse(false, 9)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -537,7 +537,7 @@ func TestModelStep(t *testing.T) {
|
|||||||
{req: leaseRevokeRequest(1), resp: leaseRevokeResponse(3)},
|
{req: leaseRevokeRequest(1), resp: leaseRevokeResponse(3)},
|
||||||
{req: putRequest("key", "4"), resp: putResponse(4)},
|
{req: putRequest("key", "4"), resp: putResponse(4)},
|
||||||
{req: getRequest("key"), resp: getResponse("4", 4)},
|
{req: getRequest("key"), resp: getResponse("4", 4)},
|
||||||
{req: txnRequest("key", "4", "5"), resp: txnResponse(true, 5)},
|
{req: compareAndSetRequest("key", "4", "5"), resp: compareAndSetResponse(true, 5)},
|
||||||
{req: deleteRequest("key"), resp: deleteResponse(1, 6)},
|
{req: deleteRequest("key"), resp: deleteResponse(1, 6)},
|
||||||
{req: defragmentRequest(), resp: defragmentResponse()},
|
{req: defragmentRequest(), resp: defragmentResponse()},
|
||||||
},
|
},
|
||||||
@ -556,7 +556,7 @@ func TestModelStep(t *testing.T) {
|
|||||||
{req: defragmentRequest(), resp: defragmentResponse()},
|
{req: defragmentRequest(), resp: defragmentResponse()},
|
||||||
{req: getRequest("key"), resp: getResponse("4", 4)},
|
{req: getRequest("key"), resp: getResponse("4", 4)},
|
||||||
{req: defragmentRequest(), resp: defragmentResponse()},
|
{req: defragmentRequest(), resp: defragmentResponse()},
|
||||||
{req: txnRequest("key", "4", "5"), resp: txnResponse(true, 5)},
|
{req: compareAndSetRequest("key", "4", "5"), resp: compareAndSetResponse(true, 5)},
|
||||||
{req: defragmentRequest(), resp: defragmentResponse()},
|
{req: defragmentRequest(), resp: defragmentResponse()},
|
||||||
{req: deleteRequest("key"), resp: deleteResponse(1, 6)},
|
{req: deleteRequest("key"), resp: deleteResponse(1, 6)},
|
||||||
{req: defragmentRequest(), resp: defragmentResponse()},
|
{req: defragmentRequest(), resp: defragmentResponse()},
|
||||||
@ -576,7 +576,7 @@ func TestModelStep(t *testing.T) {
|
|||||||
{req: defragmentRequest(), resp: failedResponse(errors.New("failed"))},
|
{req: defragmentRequest(), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: getRequest("key"), resp: getResponse("4", 4)},
|
{req: getRequest("key"), resp: getResponse("4", 4)},
|
||||||
{req: defragmentRequest(), resp: failedResponse(errors.New("failed"))},
|
{req: defragmentRequest(), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: txnRequest("key", "4", "5"), resp: txnResponse(true, 5)},
|
{req: compareAndSetRequest("key", "4", "5"), resp: compareAndSetResponse(true, 5)},
|
||||||
{req: defragmentRequest(), resp: failedResponse(errors.New("failed"))},
|
{req: defragmentRequest(), resp: failedResponse(errors.New("failed"))},
|
||||||
{req: deleteRequest("key"), resp: deleteResponse(1, 6)},
|
{req: deleteRequest("key"), resp: deleteResponse(1, 6)},
|
||||||
{req: defragmentRequest(), resp: failedResponse(errors.New("failed"))},
|
{req: defragmentRequest(), resp: failedResponse(errors.New("failed"))},
|
||||||
@ -664,20 +664,25 @@ func TestModelDescribe(t *testing.T) {
|
|||||||
expectDescribe: `delete("key6") -> err: "failed"`,
|
expectDescribe: `delete("key6") -> err: "failed"`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
req: txnRequest("key7", "7", "77"),
|
req: compareAndSetRequest("key7", "7", "77"),
|
||||||
resp: txnResponse(false, 7),
|
resp: compareAndSetResponse(false, 7),
|
||||||
expectDescribe: `if(key7=="7").then(put("key7", "77", nil)) -> txn failed, rev: 7`,
|
expectDescribe: `if(key7=="7").then(put("key7", "77", nil)) -> txn failed, rev: 7`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
req: txnRequest("key8", "8", "88"),
|
req: compareAndSetRequest("key8", "8", "88"),
|
||||||
resp: txnResponse(true, 8),
|
resp: compareAndSetResponse(true, 8),
|
||||||
expectDescribe: `if(key8=="8").then(put("key8", "88", nil)) -> ok, rev: 8`,
|
expectDescribe: `if(key8=="8").then(put("key8", "88", nil)) -> ok, rev: 8`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
req: txnRequest("key9", "9", "99"),
|
req: compareAndSetRequest("key9", "9", "99"),
|
||||||
resp: failedResponse(errors.New("failed")),
|
resp: failedResponse(errors.New("failed")),
|
||||||
expectDescribe: `if(key9=="9").then(put("key9", "99", nil)) -> err: "failed"`,
|
expectDescribe: `if(key9=="9").then(put("key9", "99", nil)) -> err: "failed"`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
req: txnRequest(nil, []EtcdOperation{{Type: Get, Key: "10"}, {Type: Put, Key: "11", Value: ValueOrHash{Value: "111"}}, {Type: Delete, Key: "12"}}),
|
||||||
|
resp: txnResponse([]EtcdOperationResult{{Value: ValueOrHash{Value: "110"}}, {}, {Deleted: 1}}, true, 10),
|
||||||
|
expectDescribe: `get("10"), put("11", "111", nil), delete("12") -> "110", ok, deleted: 1, rev: 10`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
req: defragmentRequest(),
|
req: defragmentRequest(),
|
||||||
resp: defragmentResponse(),
|
resp: defragmentResponse(),
|
||||||
@ -791,42 +796,42 @@ func TestModelResponseMatch(t *testing.T) {
|
|||||||
expectMatch: false,
|
expectMatch: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resp1: txnResponse(false, 7),
|
resp1: compareAndSetResponse(false, 7),
|
||||||
resp2: txnResponse(false, 7),
|
resp2: compareAndSetResponse(false, 7),
|
||||||
expectMatch: true,
|
expectMatch: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resp1: txnResponse(true, 7),
|
resp1: compareAndSetResponse(true, 7),
|
||||||
resp2: txnResponse(false, 7),
|
resp2: compareAndSetResponse(false, 7),
|
||||||
expectMatch: false,
|
expectMatch: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resp1: txnResponse(false, 7),
|
resp1: compareAndSetResponse(false, 7),
|
||||||
resp2: txnResponse(false, 8),
|
resp2: compareAndSetResponse(false, 8),
|
||||||
expectMatch: false,
|
expectMatch: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resp1: txnResponse(false, 7),
|
resp1: compareAndSetResponse(false, 7),
|
||||||
resp2: failedResponse(errors.New("failed request")),
|
resp2: failedResponse(errors.New("failed request")),
|
||||||
expectMatch: false,
|
expectMatch: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resp1: txnResponse(true, 7),
|
resp1: compareAndSetResponse(true, 7),
|
||||||
resp2: unknownResponse(7),
|
resp2: unknownResponse(7),
|
||||||
expectMatch: true,
|
expectMatch: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resp1: txnResponse(false, 7),
|
resp1: compareAndSetResponse(false, 7),
|
||||||
resp2: unknownResponse(7),
|
resp2: unknownResponse(7),
|
||||||
expectMatch: true,
|
expectMatch: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resp1: txnResponse(true, 7),
|
resp1: compareAndSetResponse(true, 7),
|
||||||
resp2: unknownResponse(0),
|
resp2: unknownResponse(0),
|
||||||
expectMatch: false,
|
expectMatch: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resp1: txnResponse(false, 7),
|
resp1: compareAndSetResponse(false, 7),
|
||||||
resp2: unknownResponse(0),
|
resp2: unknownResponse(0),
|
||||||
expectMatch: false,
|
expectMatch: false,
|
||||||
},
|
},
|
||||||
|
@ -24,12 +24,15 @@ import (
|
|||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
"go.etcd.io/etcd/api/v3/mvccpb"
|
"go.etcd.io/etcd/api/v3/mvccpb"
|
||||||
|
clientv3 "go.etcd.io/etcd/client/v3"
|
||||||
"go.etcd.io/etcd/tests/v3/linearizability/identity"
|
"go.etcd.io/etcd/tests/v3/linearizability/identity"
|
||||||
|
"go.etcd.io/etcd/tests/v3/linearizability/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
DefaultLeaseTTL int64 = 7200
|
DefaultLeaseTTL int64 = 7200
|
||||||
RequestTimeout = 40 * time.Millisecond
|
RequestTimeout = 40 * time.Millisecond
|
||||||
|
MultiOpTxnOpCount = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
type TrafficRequestType string
|
type TrafficRequestType string
|
||||||
@ -39,6 +42,7 @@ const (
|
|||||||
Put TrafficRequestType = "put"
|
Put TrafficRequestType = "put"
|
||||||
LargePut TrafficRequestType = "largePut"
|
LargePut TrafficRequestType = "largePut"
|
||||||
Delete TrafficRequestType = "delete"
|
Delete TrafficRequestType = "delete"
|
||||||
|
MultiOpTxn TrafficRequestType = "multiOpTxn"
|
||||||
PutWithLease TrafficRequestType = "putWithLease"
|
PutWithLease TrafficRequestType = "putWithLease"
|
||||||
LeaseRevoke TrafficRequestType = "leaseRevoke"
|
LeaseRevoke TrafficRequestType = "leaseRevoke"
|
||||||
CompareAndSet TrafficRequestType = "compareAndSet"
|
CompareAndSet TrafficRequestType = "compareAndSet"
|
||||||
@ -75,8 +79,7 @@ func (t traffic) Run(ctx context.Context, clientId int, c *recordingClient, limi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Provide each write with unique id to make it easier to validate operation history.
|
t.Write(ctx, c, limiter, key, ids, lm, clientId, resp)
|
||||||
t.Write(ctx, c, limiter, key, fmt.Sprintf("%d", ids.RequestId()), lm, clientId, resp)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,23 +93,25 @@ func (t traffic) Read(ctx context.Context, c *recordingClient, limiter *rate.Lim
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t traffic) Write(ctx context.Context, c *recordingClient, limiter *rate.Limiter, key string, newValue string, lm identity.LeaseIdStorage, cid int, lastValues []*mvccpb.KeyValue) error {
|
func (t traffic) Write(ctx context.Context, c *recordingClient, limiter *rate.Limiter, key string, id identity.Provider, lm identity.LeaseIdStorage, cid int, lastValues []*mvccpb.KeyValue) error {
|
||||||
writeCtx, cancel := context.WithTimeout(ctx, RequestTimeout)
|
writeCtx, cancel := context.WithTimeout(ctx, RequestTimeout)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
switch t.pickWriteRequest() {
|
switch t.pickWriteRequest() {
|
||||||
case Put:
|
case Put:
|
||||||
err = c.Put(writeCtx, key, newValue)
|
err = c.Put(writeCtx, key, fmt.Sprintf("%d", id.RequestId()))
|
||||||
case LargePut:
|
case LargePut:
|
||||||
err = c.Put(writeCtx, key, randString(t.largePutSize))
|
err = c.Put(writeCtx, key, randString(t.largePutSize))
|
||||||
case Delete:
|
case Delete:
|
||||||
err = c.Delete(writeCtx, key)
|
err = c.Delete(writeCtx, key)
|
||||||
|
case MultiOpTxn:
|
||||||
|
err = c.Txn(writeCtx, nil, t.pickMultiTxnOps(id))
|
||||||
case CompareAndSet:
|
case CompareAndSet:
|
||||||
var expectValue string
|
var expectValue string
|
||||||
if len(lastValues) != 0 {
|
if len(lastValues) != 0 {
|
||||||
expectValue = string(lastValues[0].Value)
|
expectValue = string(lastValues[0].Value)
|
||||||
}
|
}
|
||||||
err = c.CompareAndSet(writeCtx, key, expectValue, newValue)
|
err = c.CompareAndSet(writeCtx, key, expectValue, fmt.Sprintf("%d", id.RequestId()))
|
||||||
case PutWithLease:
|
case PutWithLease:
|
||||||
leaseId := lm.LeaseId(cid)
|
leaseId := lm.LeaseId(cid)
|
||||||
if leaseId == 0 {
|
if leaseId == 0 {
|
||||||
@ -118,7 +123,7 @@ func (t traffic) Write(ctx context.Context, c *recordingClient, limiter *rate.Li
|
|||||||
}
|
}
|
||||||
if leaseId != 0 {
|
if leaseId != 0 {
|
||||||
putCtx, putCancel := context.WithTimeout(ctx, RequestTimeout)
|
putCtx, putCancel := context.WithTimeout(ctx, RequestTimeout)
|
||||||
err = c.PutWithLease(putCtx, key, newValue, leaseId)
|
err = c.PutWithLease(putCtx, key, fmt.Sprintf("%d", id.RequestId()), leaseId)
|
||||||
putCancel()
|
putCancel()
|
||||||
}
|
}
|
||||||
case LeaseRevoke:
|
case LeaseRevoke:
|
||||||
@ -157,6 +162,50 @@ func (t traffic) pickWriteRequest() TrafficRequestType {
|
|||||||
panic("unexpected")
|
panic("unexpected")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t traffic) pickMultiTxnOps(ids identity.Provider) (ops []clientv3.Op) {
|
||||||
|
keys := rand.Perm(t.keyCount)
|
||||||
|
opTypes := make([]model.OperationType, 4)
|
||||||
|
|
||||||
|
atLeastOnePut := false
|
||||||
|
for i := 0; i < MultiOpTxnOpCount; i++ {
|
||||||
|
opTypes[i] = t.pickOperationType()
|
||||||
|
if opTypes[i] == model.Put {
|
||||||
|
atLeastOnePut = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ensure at least one put to make operation unique
|
||||||
|
if !atLeastOnePut {
|
||||||
|
opTypes[0] = model.Put
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, opType := range opTypes {
|
||||||
|
key := fmt.Sprintf("%d", keys[i])
|
||||||
|
switch opType {
|
||||||
|
case model.Get:
|
||||||
|
ops = append(ops, clientv3.OpGet(key))
|
||||||
|
case model.Put:
|
||||||
|
value := fmt.Sprintf("%d", ids.RequestId())
|
||||||
|
ops = append(ops, clientv3.OpPut(key, value))
|
||||||
|
case model.Delete:
|
||||||
|
ops = append(ops, clientv3.OpDelete(key))
|
||||||
|
default:
|
||||||
|
panic("unsuported operation type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ops
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t traffic) pickOperationType() model.OperationType {
|
||||||
|
roll := rand.Int() % 100
|
||||||
|
if roll < 10 {
|
||||||
|
return model.Delete
|
||||||
|
}
|
||||||
|
if roll < 50 {
|
||||||
|
return model.Get
|
||||||
|
}
|
||||||
|
return model.Put
|
||||||
|
}
|
||||||
|
|
||||||
func randString(size int) string {
|
func randString(size int) string {
|
||||||
data := strings.Builder{}
|
data := strings.Builder{}
|
||||||
data.Grow(size)
|
data.Grow(size)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user