diff --git a/Porcupine-LICENSE b/Porcupine-LICENSE new file mode 120000 index 0000000..89dd03d --- /dev/null +++ b/Porcupine-LICENSE @@ -0,0 +1 @@ +vendor/github.com/anishathalye/porcupine/LICENSE.md \ No newline at end of file diff --git a/cmd/rawkv/main.go b/cmd/rawkv/main.go index e5f0b64..5ae4b3b 100644 --- a/cmd/rawkv/main.go +++ b/cmd/rawkv/main.go @@ -17,7 +17,6 @@ var ( clientCase = flag.String("case", "register", "client test case, like bank,multi_bank") historyFile = flag.String("history", "./history.log", "history file") nemesises = flag.String("nemesis", "", "nemesis, seperated by name, like random_kill,all_kill") - verifyNames = flag.String("verifiers", "", "verifier names, seperate by comma, register") ) func main() { @@ -41,7 +40,7 @@ func main() { suit := util.Suit{ Config: cfg, ClientCreator: creator, - VerifyNames: *verifyNames, + ModelNames: "register", Nemesises: *nemesises, } suit.Run() diff --git a/cmd/tidb/main.go b/cmd/tidb/main.go index 3697bcf..5a192fb 100644 --- a/cmd/tidb/main.go +++ b/cmd/tidb/main.go @@ -17,7 +17,7 @@ var ( clientCase = flag.String("case", "bank", "client test case, like bank,multi_bank") historyFile = flag.String("history", "./history.log", "history file") nemesises = flag.String("nemesis", "", "nemesis, seperated by name, like random_kill,all_kill") - verifyNames = flag.String("verifiers", "", "verifier names, seperate by comma, tidb_bank,tidb_bank_tso") + modelNames = flag.String("models", "", "model names, seperate by comma, tidb_bank,tidb_bank_tso") ) func main() { @@ -43,7 +43,7 @@ func main() { suit := util.Suit{ Config: cfg, ClientCreator: creator, - VerifyNames: *verifyNames, + ModelNames: *modelNames, Nemesises: *nemesises, } suit.Run() diff --git a/cmd/txnkv/main.go b/cmd/txnkv/main.go index a8a86ee..788698e 100644 --- a/cmd/txnkv/main.go +++ b/cmd/txnkv/main.go @@ -17,7 +17,6 @@ var ( clientCase = flag.String("case", "register", "client test case, like bank,multi_bank") historyFile = flag.String("history", "./history.log", "history file") nemesises = flag.String("nemesis", "", "nemesis, seperated by name, like random_kill,all_kill") - verifyNames = flag.String("verifiers", "", "verifier names, seperate by comma, txnkv_register") ) func main() { @@ -41,7 +40,7 @@ func main() { suit := util.Suit{ Config: cfg, ClientCreator: creator, - VerifyNames: *verifyNames, + ModelNames: "register", Nemesises: *nemesises, } suit.Run() diff --git a/cmd/util/suit.go b/cmd/util/suit.go index a04e77e..8a477cc 100644 --- a/cmd/util/suit.go +++ b/cmd/util/suit.go @@ -18,8 +18,8 @@ import ( type Suit struct { control.Config core.ClientCreator - // verifier names, seperate by comma. - VerifyNames string + // model names, seperate by comma. + ModelNames string // nemesis, seperated by comma. Nemesises string } @@ -61,5 +61,5 @@ func (suit *Suit) Run() { c.Run() - verify.Verify(ctx, suit.Config.History, suit.VerifyNames) + verify.Verify(ctx, suit.Config.History, suit.ModelNames) } diff --git a/cmd/verifier/main.go b/cmd/verifier/main.go index 863f070..4435e39 100644 --- a/cmd/verifier/main.go +++ b/cmd/verifier/main.go @@ -14,7 +14,7 @@ import ( var ( historyFile = flag.String("history", "./history.log", "history file") - names = flag.String("names", "", "verifier names, seperate by comma") + names = flag.String("names", "", "model names, seperate by comma") pprofAddr = flag.String("pprof", "0.0.0.0:6060", "Pprof address") ) diff --git a/cmd/verifier/verify/util.go b/cmd/verifier/verify/util.go index 6df879e..373f3d1 100644 --- a/cmd/verifier/verify/util.go +++ b/cmd/verifier/verify/util.go @@ -6,23 +6,32 @@ import ( "strings" "github.com/siddontang/chaos/db/tidb" + "github.com/siddontang/chaos/pkg/check/porcupine" + "github.com/siddontang/chaos/pkg/core" "github.com/siddontang/chaos/pkg/history" "github.com/siddontang/chaos/pkg/model" ) -// Verify creates the verifier from verifer_names and verfies the history file. -func Verify(ctx context.Context, historyFile string, verfier_names string) { - var verifieres []history.Verifier +type suit struct { + checker core.Checker + model core.Model + parser history.RecordParser +} + +// Verify creates the verifier from model name and verfies the history file. +func Verify(ctx context.Context, historyFile string, modelNames string) { + var suits []suit - for _, name := range strings.Split(verfier_names, ",") { - var verifier history.Verifier + for _, name := range strings.Split(modelNames, ",") { + s := suit{} switch name { case "tidb_bank": - verifier = tidb.BankVerifier{} + s.model, s.parser, s.checker = tidb.BankModel(), tidb.BankParser(), porcupine.Checker{} case "tidb_bank_tso": - verifier = tidb.BankTsoVerifier{} + // Actually we can omit BankModel, since BankTsoChecker does not require a Model. + s.model, s.parser, s.checker = tidb.BankModel(), tidb.BankParser(), tidb.BankTsoChecker() case "register": - verifier = model.RegisterVerifier{} + s.model, s.parser, s.checker = model.RegisterModel(), model.RegisterParser(), porcupine.Checker{} case "": continue default: @@ -30,23 +39,33 @@ func Verify(ctx context.Context, historyFile string, verfier_names string) { continue } - verifieres = append(verifieres, verifier) + suits = append(suits, s) } childCtx, cancel := context.WithCancel(ctx) go func() { - for _, verifier := range verifieres { - log.Printf("begin to check with %s", verifier.Name()) - ok, err := verifier.Verify(historyFile) + for _, suit := range suits { + log.Printf("begin to check %s with %s", suit.model.Name(), suit.checker.Name()) + ops, err := history.ReadHistory(historyFile, suit.parser) + if err != nil { + log.Fatalf("read history failed %v", err) + } + + ops, err = history.CompleteOperations(ops, suit.parser) + if err != nil { + log.Fatalf("complete history failed %v", err) + } + + ok, err := suit.checker.Check(suit.model, ops) if err != nil { log.Fatalf("verify history failed %v", err) } if !ok { - log.Fatalf("%s: history %s is not linearizable", verifier.Name(), historyFile) + log.Fatalf("%s: history %s is not linearizable", suit.model.Name(), historyFile) } else { - log.Printf("%s: history %s is linearizable", verifier.Name(), historyFile) + log.Printf("%s: history %s is linearizable", suit.model.Name(), historyFile) } } diff --git a/db/tidb/bank.go b/db/tidb/bank.go index 298be6c..08de8f9 100644 --- a/db/tidb/bank.go +++ b/db/tidb/bank.go @@ -11,11 +11,12 @@ import ( "time" "github.com/anishathalye/porcupine" + pchecker "github.com/siddontang/chaos/pkg/check/porcupine" + "github.com/siddontang/chaos/pkg/core" + "github.com/siddontang/chaos/pkg/history" // use mysql _ "github.com/go-sql-driver/mysql" - "github.com/siddontang/chaos/pkg/core" - "github.com/siddontang/chaos/pkg/history" ) const ( @@ -197,14 +198,6 @@ func (r bankResponse) IsUnknown() bool { return r.Unknown } -func newBankEvent(v interface{}, id uint) porcupine.Event { - if _, ok := v.(bankRequest); ok { - return porcupine.Event{Kind: porcupine.CallEvent, Value: v, Id: id} - } - - return porcupine.Event{Kind: porcupine.ReturnEvent, Value: v, Id: id} -} - func balancesEqual(a, b []int64) bool { if len(a) != len(b) { return false @@ -219,54 +212,67 @@ func balancesEqual(a, b []int64) bool { return true } -func getBankModel(n int) porcupine.Model { - return porcupine.Model{ - Init: func() interface{} { - v := make([]int64, n) - for i := 0; i < n; i++ { - v[i] = initBalance - } - return v - }, - Step: func(state interface{}, input interface{}, output interface{}) (bool, interface{}) { - st := state.([]int64) - inp := input.(bankRequest) - out := output.(bankResponse) - - if inp.Op == 0 { - // read - ok := out.Unknown || balancesEqual(st, out.Balances) - return ok, state - } +type bank struct { + accountNum int +} - // for transfer - if !out.Ok && !out.Unknown { - return true, state - } +func (b bank) Init() interface{} { + v := make([]int64, b.accountNum) + for i := 0; i < b.accountNum; i++ { + v[i] = initBalance + } + return v +} + +func (bank) Step(state interface{}, input interface{}, output interface{}) (bool, interface{}) { + st := state.([]int64) + inp := input.(bankRequest) + out := output.(bankResponse) - newSt := append([]int64{}, st...) - newSt[inp.From] -= inp.Amount - newSt[inp.To] += inp.Amount - return out.Ok || out.Unknown, newSt - }, + if inp.Op == 0 { + // read + ok := out.Unknown || balancesEqual(st, out.Balances) + return ok, state + } - Equal: func(state1, state2 interface{}) bool { - st1 := state1.([]int64) - st2 := state2.([]int64) - return balancesEqual(st1, st2) - }, + // for transfer + if !out.Ok && !out.Unknown { + return true, state } + + newSt := append([]int64{}, st...) + newSt[inp.From] -= inp.Amount + newSt[inp.To] += inp.Amount + return out.Ok || out.Unknown, newSt +} + +func (bank) Equal(state1, state2 interface{}) bool { + st1 := state1.([]int64) + st2 := state2.([]int64) + return balancesEqual(st1, st2) +} + +func (bank) Name() string { + return "tidb_bank" } -type bankParser struct { +// BankModel is the model of bank in TiDB +func BankModel() core.Model { + return bank{ + accountNum: accountNum, + } } +type bankParser struct{} + +// OnRequest impls history.RecordParser.OnRequest func (p bankParser) OnRequest(data json.RawMessage) (interface{}, error) { r := bankRequest{} err := json.Unmarshal(data, &r) return r, err } +// OnResponse impls history.RecordParser.OnRequest func (p bankParser) OnResponse(data json.RawMessage) (interface{}, error) { r := bankResponse{} err := json.Unmarshal(data, &r) @@ -276,10 +282,16 @@ func (p bankParser) OnResponse(data json.RawMessage) (interface{}, error) { return r, err } +// OnNoopResponse impls history.RecordParser.OnRequest func (p bankParser) OnNoopResponse() interface{} { return bankResponse{Unknown: true} } +// BankParser parses a history of bank operations. +func BankParser() history.RecordParser { + return bankParser{} +} + // BankClientCreator creates a bank test client for tidb. type BankClientCreator struct { } @@ -291,26 +303,6 @@ func (BankClientCreator) Create(node string) core.Client { } } -// BankVerifier verifies the bank history. -type BankVerifier struct { -} - -// Verify verifies the bank history. -func (BankVerifier) Verify(historyFile string) (bool, error) { - return history.IsLinearizable(historyFile, getBankModel(accountNum), bankParser{}) -} - -// Name returns the name of the verifier. -func (BankVerifier) Name() string { - return "bank_verifier" -} - -// BankTsoVerifier verifies the bank history. -// Unlike BankVerifier using porcupine, it uses a direct way because we know every timestamp of the transaction. -// So we can order all transactions with timetamp and replay them. -type BankTsoVerifier struct { -} - type tsoEvent struct { Tso uint64 Op int @@ -349,15 +341,7 @@ func (s tsoEvents) Len() int { return len(s) } func (s tsoEvents) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s tsoEvents) Less(i, j int) bool { return s[i].Tso < s[j].Tso } -func parseTsoEvents(historyFile string) (tsoEvents, error) { - events, err := history.ParseEvents(historyFile, bankParser{}) - if err != nil { - return nil, err - } - - return generateTsoEvents(events), nil -} - +// TODO: remove porcupine dependence. func generateTsoEvents(events []porcupine.Event) tsoEvents { tEvents := make(tsoEvents, 0, len(events)) @@ -554,17 +538,27 @@ func verifyTsoEvents(events tsoEvents) bool { return true } -// Verify verifes the bank history. -func (BankTsoVerifier) Verify(historyFile string) (bool, error) { - events, err := parseTsoEvents(historyFile) +// bankTsoChecker uses a direct way because we know every timestamp of the transaction. +// So we can order all transactions with timetamp and replay them. +type bankTsoChecker struct { +} + +// Check checks the bank history. +func (bankTsoChecker) Check(_ core.Model, ops []core.Operation) (bool, error) { + events, err := pchecker.ConvertOperationsToEvents(ops) if err != nil { return false, err } - - return verifyTsoEvents(events), nil + tEvents := generateTsoEvents(events) + return verifyTsoEvents(tEvents), nil } // Name returns the name of the verifier. -func (BankTsoVerifier) Name() string { - return "bank_tso_verifier" +func (bankTsoChecker) Name() string { + return "tidb_bank_tso_checker" +} + +// BankTsoChecker checks the bank history with the help of tso. +func BankTsoChecker() core.Checker { + return bankTsoChecker{} } diff --git a/db/tidb/bank_test.go b/db/tidb/bank_test.go index fd55c6f..87e308b 100644 --- a/db/tidb/bank_test.go +++ b/db/tidb/bank_test.go @@ -11,6 +11,25 @@ func checkTsoEvents(evnets []porcupine.Event) bool { return verifyTsoEvents(tEvents) } +func getBankModel(accountNum int) porcupine.Model { + m := bank{ + accountNum: 2, + } + return porcupine.Model{ + Init: m.Init, + Step: m.Step, + Equal: m.Equal, + } +} + +func newBankEvent(v interface{}, id uint) porcupine.Event { + if _, ok := v.(bankRequest); ok { + return porcupine.Event{Kind: porcupine.CallEvent, Value: v, Id: id} + } + + return porcupine.Event{Kind: porcupine.ReturnEvent, Value: v, Id: id} +} + func TestBankVerify(t *testing.T) { m := getBankModel(2) diff --git a/pkg/check/doc.go b/pkg/check/doc.go new file mode 100644 index 0000000..1a0b3e8 --- /dev/null +++ b/pkg/check/doc.go @@ -0,0 +1,3 @@ +// check is the place to put checkers. + +package check diff --git a/pkg/check/porcupine/porcupine.go b/pkg/check/porcupine/porcupine.go new file mode 100644 index 0000000..e3c63fa --- /dev/null +++ b/pkg/check/porcupine/porcupine.go @@ -0,0 +1,76 @@ +package porcupine + +import ( + "log" + + "github.com/pkg/errors" + + "github.com/anishathalye/porcupine" + "github.com/siddontang/chaos/pkg/core" +) + +// Checker is a linearizability checker powered by Porcupine. +type Checker struct{} + +// Check checks the history of operations meets liearizability or not with model. +// False means the history is not linearizable. +func (Checker) Check(m core.Model, ops []core.Operation) (bool, error) { + pModel := porcupine.Model{ + Init: m.Init, + Step: m.Step, + Equal: m.Equal, + } + events, err := ConvertOperationsToEvents(ops) + if err != nil { + return false, err + } + log.Printf("begin to verify %d events", len(events)) + return porcupine.CheckEvents(pModel, events), nil +} + +// Name is the name of porcupine checker +func (Checker) Name() string { + return "porcupine_checker" +} + +// ConvertOperationsToEvents converts core.Operations to porcupine.Event. +func ConvertOperationsToEvents(ops []core.Operation) ([]porcupine.Event, error) { + if len(ops)%2 != 0 { + return nil, errors.New("history is not complete") + } + + procID := map[int64]uint{} + id := uint(0) + events := make([]porcupine.Event, 0, len(ops)) + for _, op := range ops { + if op.Action == core.InvokeOperation { + event := porcupine.Event{ + Kind: porcupine.CallEvent, + Id: id, + Value: op.Data, + } + events = append(events, event) + procID[op.Proc] = id + id++ + } else { + if op.Data == nil { + continue + } + + matchID := procID[op.Proc] + delete(procID, op.Proc) + event := porcupine.Event{ + Kind: porcupine.ReturnEvent, + Id: matchID, + Value: op.Data, + } + events = append(events, event) + } + } + + if len(procID) != 0 { + return nil, errors.New("history is not complete") + } + + return events, nil +} diff --git a/pkg/check/porcupine/porcupine_test.go b/pkg/check/porcupine/porcupine_test.go new file mode 100644 index 0000000..a9ba009 --- /dev/null +++ b/pkg/check/porcupine/porcupine_test.go @@ -0,0 +1,71 @@ +package porcupine + +import ( + "testing" + + "github.com/siddontang/chaos/pkg/core" +) + +type noopRequest struct { + // 0 for read, 1 for write + Op int + Value int +} + +type noopResponse struct { + Value int + Ok bool + Unknown bool +} + +type noop struct{} + +func (noop) Init() interface{} { + return 10 +} + +func (noop) Step(state interface{}, input interface{}, output interface{}) (bool, interface{}) { + st := state.(int) + inp := input.(noopRequest) + out := output.(noopResponse) + + if inp.Op == 0 { + // read + ok := out.Unknown || st == out.Value + return ok, state + } + + // for write + return out.Ok || out.Unknown, inp.Value +} + +func (noop) Equal(state1, state2 interface{}) bool { + s1 := state1.(int) + s2 := state2.(int) + + return s1 == s2 +} + +func (noop) Name() string { + return "noop" +} + +func TestPorcupineChecker(t *testing.T) { + ops := []core.Operation{ + {core.InvokeOperation, 1, noopRequest{Op: 0}}, + {core.ReturnOperation, 1, noopResponse{Value: 10}}, + {core.InvokeOperation, 2, noopRequest{Op: 1, Value: 15}}, + {core.ReturnOperation, 2, noopResponse{Unknown: true}}, + {core.InvokeOperation, 3, noopRequest{Op: 0}}, + {core.ReturnOperation, 3, noopResponse{Value: 15}}, + } + + var checker Checker + ok, err := checker.Check(noop{}, ops) + if err != nil { + t.Fatalf("verify history failed %v", err) + } + if !ok { + t.Fatal("must be linearizable") + } +} diff --git a/pkg/core/checker.go b/pkg/core/checker.go new file mode 100644 index 0000000..3032fcb --- /dev/null +++ b/pkg/core/checker.go @@ -0,0 +1,11 @@ +package core + +// Checker checks a history of operations. +type Checker interface { + // Check a series of operations with the given model. + // Return false or error if operations do not satisfy the model. + Check(m Model, ops []Operation) (bool, error) + + // Name returns the unique name for the checker. + Name() string +} diff --git a/pkg/core/model.go b/pkg/core/model.go new file mode 100644 index 0000000..caa091a --- /dev/null +++ b/pkg/core/model.go @@ -0,0 +1,31 @@ +package core + +// Model specifies the behavior of a data object. +type Model interface { + // Initial state of the data object. + Init() interface{} + + // Step function for the data object. Returns whether or not the system + // could take this step with the given inputs and outputs and also + // returns the new state. This should not mutate the existing state. + Step(state interface{}, input interface{}, output interface{}) (bool, interface{}) + + // Equality on states. + Equal(state1, state2 interface{}) bool + + // Name returns the unique name for the model. + Name() string +} + +// Operation action +const ( + InvokeOperation = "call" + ReturnOperation = "return" +) + +// Operation of a data object. +type Operation struct { + Action string `json:"action"` + Proc int64 `json:"proc"` + Data interface{} `json:"data"` +} diff --git a/pkg/history/history.go b/pkg/history/history.go index bc30360..688b88b 100644 --- a/pkg/history/history.go +++ b/pkg/history/history.go @@ -3,30 +3,28 @@ package history import ( "bufio" "encoding/json" - "log" + "fmt" "os" "path" + "sort" "sync" - "github.com/anishathalye/porcupine" + "github.com/siddontang/chaos/pkg/core" ) -// Operation action -const ( - InvokeOperation = "call" - ReturnOperation = "return" -) - -type operation struct { +// opRecord is similar to core.Operation, but it stores data in json.RawMessage +// instead of interface{} in order to marshal into bytes. +type opRecord struct { Action string `json:"action"` Proc int64 `json:"proc"` Data json.RawMessage `json:"data"` } -// Recorder records operation histogry. +// Recorder records operation history. type Recorder struct { sync.Mutex - f *os.File + f *os.File + ops []core.Operation } // NewRecorder creates a recorder to log the history to the file. @@ -48,21 +46,22 @@ func (r *Recorder) Close() { // RecordRequest records the request. func (r *Recorder) RecordRequest(proc int64, op interface{}) error { - return r.record(proc, InvokeOperation, op) + return r.record(proc, core.InvokeOperation, op) } // RecordResponse records the response. func (r *Recorder) RecordResponse(proc int64, op interface{}) error { - return r.record(proc, ReturnOperation, op) + return r.record(proc, core.ReturnOperation, op) } func (r *Recorder) record(proc int64, action string, op interface{}) error { + // Marshal the op to json in order to store it in a history file. data, err := json.Marshal(op) if err != nil { return err } - v := operation{ + v := opRecord{ Action: action, Proc: proc, Data: json.RawMessage(data), @@ -84,15 +83,27 @@ func (r *Recorder) record(proc int64, action string, op interface{}) error { return err } + // Store the op in core.Operation directly. + coreOp := core.Operation{ + Action: action, + Proc: proc, + Data: op, + } + r.ops = append(r.ops, coreOp) return nil } +// Operations returns operations that it records +func (r *Recorder) Operations() []core.Operation { + return r.ops +} + // RecordParser is to parses the operation data. type RecordParser interface { - // OnRequest parses the request record. + // OnRequest parses an operation data to model's input. OnRequest(data json.RawMessage) (interface{}, error) - // OnResponse parses the response record. Return nil means - // the operation has an infinite end time. + // OnResponse parses an operation data to model's output. + // Return nil means the operation has an infinite end time. // E.g, we meet timeout for a operation. OnResponse(data json.RawMessage) (interface{}, error) // If we have some infinite operations, we should return a @@ -100,89 +111,93 @@ type RecordParser interface { OnNoopResponse() interface{} } -// Verifier verifies the history. -type Verifier interface { - Verify(historyFile string) (bool, error) - Name() string -} - -// IsLinearizable checks the history file meets liearizability or not with model. -// False means the history is not linearizable. -func IsLinearizable(historyFile string, m porcupine.Model, p RecordParser) (bool, error) { - events, err := ParseEvents(historyFile, p) - if err != nil { - return false, err - } - log.Printf("begin to verify %d events", len(events)) - return porcupine.CheckEvents(m, events), nil -} - -// ParseEvents parses the history and returns a procupine Event list. -func ParseEvents(historyFile string, p RecordParser) ([]porcupine.Event, error) { +// ReadHistory reads operations from a history file. +func ReadHistory(historyFile string, p RecordParser) ([]core.Operation, error) { f, err := os.Open(historyFile) if err != nil { return nil, err } defer f.Close() - procID := map[int64]uint{} - id := uint(0) - - events := make([]porcupine.Event, 0, 1024) + ops := make([]core.Operation, 0, 1024) scanner := bufio.NewScanner(f) for scanner.Scan() { - var op operation - if err = json.Unmarshal(scanner.Bytes(), &op); err != nil { + var record opRecord + if err = json.Unmarshal(scanner.Bytes(), &record); err != nil { return nil, err } - var value interface{} - if op.Action == InvokeOperation { - if value, err = p.OnRequest(op.Data); err != nil { + var data interface{} + if record.Action == core.InvokeOperation { + if data, err = p.OnRequest(record.Data); err != nil { return nil, err } - - event := porcupine.Event{ - Kind: porcupine.CallEvent, - Id: id, - Value: value, - } - events = append(events, event) - procID[op.Proc] = id - id++ } else { - if value, err = p.OnResponse(op.Data); err != nil { + if data, err = p.OnResponse(record.Data); err != nil { return nil, err } + } - if value == nil { + op := core.Operation{ + Action: record.Action, + Proc: record.Proc, + Data: data, + } + ops = append(ops, op) + } + + if err = scanner.Err(); err != nil { + return nil, err + } + + return ops, nil +} + +// int64Slice attaches the methods of Interface to []int, sorting in increasing order. +type int64Slice []int64 + +func (p int64Slice) Len() int { return len(p) } +func (p int64Slice) Less(i, j int) bool { return p[i] < p[j] } +func (p int64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + +// CompleteOperations completes the history of operation. +func CompleteOperations(ops []core.Operation, p RecordParser) ([]core.Operation, error) { + procID := map[int64]struct{}{} + compOps := make([]core.Operation, 0, len(ops)) + for _, op := range ops { + if op.Action == core.InvokeOperation { + if _, ok := procID[op.Proc]; ok { + return nil, fmt.Errorf("missing return, op: %v", op) + } + procID[op.Proc] = struct{}{} + compOps = append(compOps, op) + } else { + if _, ok := procID[op.Proc]; !ok { + return nil, fmt.Errorf("missing invoke, op: %v", op) + } + if op.Data == nil { continue } - - matchID := procID[op.Proc] delete(procID, op.Proc) - event := porcupine.Event{ - Kind: porcupine.ReturnEvent, - Id: matchID, - Value: value, - } - events = append(events, event) + compOps = append(compOps, op) } } - if err = scanner.Err(); err != nil { - return nil, err + // To get a determined complete history of operations, we sort procIDs. + var keys []int64 + for k := range procID { + keys = append(keys, k) } + sort.Sort(int64Slice(keys)) - for _, id := range procID { - response := p.OnNoopResponse() - event := porcupine.Event{ - Kind: porcupine.ReturnEvent, - Id: id, - Value: response, + for _, proc := range keys { + op := core.Operation{ + Action: core.ReturnOperation, + Proc: proc, + Data: p.OnNoopResponse(), } - events = append(events, event) + compOps = append(compOps, op) } - return events, nil + return compOps, nil } diff --git a/pkg/history/history_test.go b/pkg/history/history_test.go index 3084f67..878d879 100644 --- a/pkg/history/history_test.go +++ b/pkg/history/history_test.go @@ -7,7 +7,7 @@ import ( "path" "testing" - "github.com/anishathalye/porcupine" + "github.com/siddontang/chaos/pkg/core" ) type noopRequest struct { @@ -22,26 +22,9 @@ type noopResponse struct { Unknown bool } -func getNoopModel() porcupine.Model { - return porcupine.Model{ - Init: func() interface{} { - return 10 - }, - Step: func(state interface{}, input interface{}, output interface{}) (bool, interface{}) { - st := state.(int) - inp := input.(noopRequest) - out := output.(noopResponse) - - if inp.Op == 0 { - // read - ok := out.Unknown || st == out.Value - return ok, state - } - - // for write - return out.Ok || out.Unknown, inp.Value - }, - } +type action struct { + proc int64 + op interface{} } type noopParser struct { @@ -66,7 +49,7 @@ func (p noopParser) OnNoopResponse() interface{} { return noopResponse{Unknown: true} } -func TestHistory(t *testing.T) { +func TestRecordAndReadHistory(t *testing.T) { tmpDir, err := ioutil.TempDir(".", "var") if err != nil { t.Fatalf("create temp dir failed %v", err) @@ -83,14 +66,11 @@ func TestHistory(t *testing.T) { defer r.Close() - actions := []struct { - proc int64 - op interface{} - }{ + actions := []action{ {1, noopRequest{Op: 0}}, {1, noopResponse{Value: 10}}, {2, noopRequest{Op: 1, Value: 15}}, - {2, noopResponse{Unknown: true}}, + {2, noopResponse{Value: 15}}, {3, noopRequest{Op: 0}}, {3, noopResponse{Value: 15}}, } @@ -108,15 +88,136 @@ func TestHistory(t *testing.T) { } } - r.Close() + historyOps, err := ReadHistory(name, noopParser{}) + if err != nil { + t.Fatal(err) + } + + tbl := [][]core.Operation{historyOps, r.Operations()} + + for _, ops := range tbl { + if len(ops) != len(actions) { + t.Fatalf("actions %v mismatchs ops %v", actions, ops) + } + + for idx, ac := range actions { + switch v := ac.op.(type) { + case noopRequest: + a, ok := ops[idx].Data.(noopRequest) + if !ok { + t.Fatalf("unexpected: %#v", ops[idx]) + } + if a != v { + t.Fatalf("actions %#v mismatchs ops %#v", a, ops[idx]) + } + case noopResponse: + a, ok := ops[idx].Data.(noopResponse) + if !ok { + t.Fatalf("unexpected: %#v", ops[idx]) + } + if a != v { + t.Fatalf("actions %#v mismatchs ops %#v", a, ops[idx]) + } + } + } + } +} + +func TestCompleteOperation(t *testing.T) { + cases := []struct { + ops []core.Operation + compOps []core.Operation + }{ + // A complete history of operations. + { + ops: []core.Operation{ + {core.InvokeOperation, 1, noopRequest{Op: 0}}, + {core.ReturnOperation, 1, noopResponse{Value: 10}}, + {core.InvokeOperation, 2, noopRequest{Op: 1, Value: 15}}, + {core.ReturnOperation, 2, noopResponse{Value: 15}}, + }, + compOps: []core.Operation{ + {core.InvokeOperation, 1, noopRequest{Op: 0}}, + {core.ReturnOperation, 1, noopResponse{Value: 10}}, + {core.InvokeOperation, 2, noopRequest{Op: 1, Value: 15}}, + {core.ReturnOperation, 2, noopResponse{Value: 15}}, + }, + }, + // A complete but repeated proc operations. + { + ops: []core.Operation{ + {core.InvokeOperation, 1, noopRequest{Op: 0}}, + {core.ReturnOperation, 1, noopResponse{Value: 10}}, + {core.InvokeOperation, 2, noopRequest{Op: 1, Value: 15}}, + {core.ReturnOperation, 2, noopResponse{Value: 15}}, + {core.InvokeOperation, 1, noopRequest{Op: 0}}, + {core.ReturnOperation, 1, noopResponse{Value: 15}}, + }, + compOps: []core.Operation{ + {core.InvokeOperation, 1, noopRequest{Op: 0}}, + {core.ReturnOperation, 1, noopResponse{Value: 10}}, + {core.InvokeOperation, 2, noopRequest{Op: 1, Value: 15}}, + {core.ReturnOperation, 2, noopResponse{Value: 15}}, + {core.InvokeOperation, 1, noopRequest{Op: 0}}, + {core.ReturnOperation, 1, noopResponse{Value: 15}}, + }, + }, + + // Pending requests. + { + ops: []core.Operation{ + {core.InvokeOperation, 1, noopRequest{Op: 0}}, + {core.ReturnOperation, 1, nil}, + }, + compOps: []core.Operation{ + {core.InvokeOperation, 1, noopRequest{Op: 0}}, + {core.ReturnOperation, 1, noopResponse{Unknown: true}}, + }, + }, + + // Missing a response + { + ops: []core.Operation{ + {core.InvokeOperation, 1, noopRequest{Op: 0}}, + }, + compOps: []core.Operation{ + {core.InvokeOperation, 1, noopRequest{Op: 0}}, + {core.ReturnOperation, 1, noopResponse{Unknown: true}}, + }, + }, - m := getNoopModel() - var ok bool - if ok, err = IsLinearizable(name, m, noopParser{}); err != nil { - t.Fatalf("verify history failed %v", err) + // A complex out of order history. + { + ops: []core.Operation{ + {core.InvokeOperation, 1, noopRequest{Op: 0}}, + {core.InvokeOperation, 3, noopRequest{Op: 0}}, + {core.InvokeOperation, 2, noopRequest{Op: 1, Value: 15}}, + {core.ReturnOperation, 2, nil}, + {core.InvokeOperation, 4, noopRequest{Op: 1, Value: 16}}, + {core.ReturnOperation, 3, nil}, + }, + compOps: []core.Operation{ + {core.InvokeOperation, 1, noopRequest{Op: 0}}, + {core.InvokeOperation, 3, noopRequest{Op: 0}}, + {core.InvokeOperation, 2, noopRequest{Op: 1, Value: 15}}, + {core.InvokeOperation, 4, noopRequest{Op: 1, Value: 16}}, + {core.ReturnOperation, 1, noopResponse{Unknown: true}}, + {core.ReturnOperation, 2, noopResponse{Unknown: true}}, + {core.ReturnOperation, 3, noopResponse{Unknown: true}}, + {core.ReturnOperation, 4, noopResponse{Unknown: true}}, + }, + }, } - if !ok { - t.Fatal("must be linearizable") + for i, cs := range cases { + compOps, err := CompleteOperations(cs.ops, noopParser{}) + if err != nil { + t.Fatalf("err: %s, case %#v", err, cs) + } + for idx, op := range compOps { + if op != cs.compOps[idx] { + t.Fatalf("op %#v, compOps %#v, case %d", op, cs.compOps[idx], i) + } + } } } diff --git a/pkg/model/cas_register.go b/pkg/model/cas_register.go index 2057616..b1e1e1f 100644 --- a/pkg/model/cas_register.go +++ b/pkg/model/cas_register.go @@ -3,7 +3,6 @@ package model import ( "encoding/json" - "github.com/anishathalye/porcupine" "github.com/siddontang/chaos/pkg/core" "github.com/siddontang/chaos/pkg/history" ) @@ -40,40 +39,47 @@ func (r CasRegisterResponse) IsUnknown() bool { return r.Unknown } -// CasRegisterModel returns a cas register model -func CasRegisterModel() porcupine.Model { - return porcupine.Model{ - Init: func() interface{} { - return -1 - }, - Step: func(state interface{}, input interface{}, output interface{}) (bool, interface{}) { - st := state.(int) - inp := input.(CasRegisterRequest) - out := output.(CasRegisterResponse) - if inp.Op == CasRegisterRead { - // read - ok := (out.Exists == false && st == -1) || (out.Exists == true && st == out.Value) || out.Unknown - return ok, state - } else if inp.Op == CasRegisterWrite { - // write - return true, inp.Arg1 - } - - // cas - ok := (inp.Arg1 == st && out.Ok) || (inp.Arg1 != st && !out.Ok) || out.Unknown - result := st - if inp.Arg1 == st { - result = inp.Arg2 - } - return ok, result - }, - - Equal: func(state1, state2 interface{}) bool { - st1 := state1.(int) - st2 := state2.(int) - return st1 == st2 - }, +type casRegister struct{} + +func (casRegister) Init() interface{} { + return -1 +} + +func (casRegister) Step(state interface{}, input interface{}, output interface{}) (bool, interface{}) { + st := state.(int) + inp := input.(CasRegisterRequest) + out := output.(CasRegisterResponse) + if inp.Op == CasRegisterRead { + // read + ok := (out.Exists == false && st == -1) || (out.Exists == true && st == out.Value) || out.Unknown + return ok, state + } else if inp.Op == CasRegisterWrite { + // write + return true, inp.Arg1 } + + // cas + ok := (inp.Arg1 == st && out.Ok) || (inp.Arg1 != st && !out.Ok) || out.Unknown + result := st + if inp.Arg1 == st { + result = inp.Arg2 + } + return ok, result +} + +func (casRegister) Equal(state1, state2 interface{}) bool { + st1 := state1.(int) + st2 := state2.(int) + return st1 == st2 +} + +func (casRegister) Name() string { + return "cas_register" +} + +// CasRegisterModel returns a cas register model +func CasRegisterModel() core.Model { + return casRegister{} } type casRegisterParser struct { @@ -98,16 +104,7 @@ func (p casRegisterParser) OnNoopResponse() interface{} { return CasRegisterResponse{Unknown: true} } -// CasRegisterVerifier can verify a cas register history. -type CasRegisterVerifier struct { -} - -// Verify verifies a cas register history. -func (CasRegisterVerifier) Verify(historyFile string) (bool, error) { - return history.IsLinearizable(historyFile, CasRegisterModel(), casRegisterParser{}) -} - -// Name returns the name of the verifier. -func (CasRegisterVerifier) Name() string { - return "cas_register_verifier" +// CasRegisterParser parses CasRegister history. +func CasRegisterParser() history.RecordParser { + return casRegisterParser{} } diff --git a/pkg/model/cas_register_test.go b/pkg/model/cas_register_test.go index f7ed008..8465288 100644 --- a/pkg/model/cas_register_test.go +++ b/pkg/model/cas_register_test.go @@ -4,8 +4,17 @@ import ( "testing" "github.com/anishathalye/porcupine" + "github.com/siddontang/chaos/pkg/core" ) +func convertModel(m core.Model) porcupine.Model { + return porcupine.Model{ + Init: m.Init, + Step: m.Step, + Equal: m.Equal, + } +} + func TestCasRegisterModel(t *testing.T) { events := []porcupine.Event{ {Kind: porcupine.CallEvent, Value: CasRegisterRequest{Op: CasRegisterWrite, Arg1: 100, Arg2: 0}, Id: 0}, @@ -19,7 +28,7 @@ func TestCasRegisterModel(t *testing.T) { {Kind: porcupine.ReturnEvent, Value: CasRegisterResponse{Ok: true}, Id: 3}, {Kind: porcupine.ReturnEvent, Value: CasRegisterResponse{Value: 200}, Id: 4}, } - res := porcupine.CheckEvents(CasRegisterModel(), events) + res := porcupine.CheckEvents(convertModel(CasRegisterModel()), events) if res != true { t.Fatal("expected operations to be linearizable") } diff --git a/pkg/model/register.go b/pkg/model/register.go index e9ec066..cb73004 100644 --- a/pkg/model/register.go +++ b/pkg/model/register.go @@ -3,7 +3,6 @@ package model import ( "encoding/json" - "github.com/anishathalye/porcupine" "github.com/siddontang/chaos/pkg/core" "github.com/siddontang/chaos/pkg/history" ) @@ -37,34 +36,41 @@ func (r RegisterResponse) IsUnknown() bool { return r.Unknown } -// RegisterModel returns a read/write register model -func RegisterModel() porcupine.Model { - return porcupine.Model{ - Init: func() interface{} { - state := 0 - return state - }, - Step: func(state interface{}, input interface{}, output interface{}) (bool, interface{}) { - st := state.(int) - inp := input.(RegisterRequest) - out := output.(RegisterResponse) - - // read - if inp.Op == RegisterRead { - ok := out.Value == st || out.Unknown - return ok, st - } - - // write - return true, inp.Value - }, - - Equal: func(state1, state2 interface{}) bool { - st1 := state1.(int) - st2 := state2.(int) - return st1 == st2 - }, +type register struct{} + +func (register) Init() interface{} { + state := 0 + return state +} + +func (register) Step(state interface{}, input interface{}, output interface{}) (bool, interface{}) { + st := state.(int) + inp := input.(RegisterRequest) + out := output.(RegisterResponse) + + // read + if inp.Op == RegisterRead { + ok := out.Value == st || out.Unknown + return ok, st } + + // write + return true, inp.Value +} + +func (register) Equal(state1, state2 interface{}) bool { + st1 := state1.(int) + st2 := state2.(int) + return st1 == st2 +} + +func (register) Name() string { + return "register" +} + +// RegisterModel returns a read/write register model +func RegisterModel() core.Model { + return register{} } type registerParser struct { @@ -89,16 +95,7 @@ func (p registerParser) OnNoopResponse() interface{} { return RegisterResponse{Unknown: true} } -// RegisterVerifier can verify a register history. -type RegisterVerifier struct { -} - -// Verify verifies a register history. -func (RegisterVerifier) Verify(historyFile string) (bool, error) { - return history.IsLinearizable(historyFile, RegisterModel(), registerParser{}) -} - -// Name returns the name of the verifier. -func (RegisterVerifier) Name() string { - return "register_verifier" +// RegisterParser parses Register history. +func RegisterParser() history.RecordParser { + return registerParser{} } diff --git a/pkg/model/register_test.go b/pkg/model/register_test.go index 5c6e8f5..4aa9b5d 100644 --- a/pkg/model/register_test.go +++ b/pkg/model/register_test.go @@ -17,7 +17,7 @@ func TestRegisterModel(t *testing.T) { {RegisterRequest{RegisterRead, 0}, 25, RegisterResponse{false, 100}, 75}, {RegisterRequest{RegisterRead, 0}, 30, RegisterResponse{false, 0}, 60}, } - res := porcupine.CheckOperations(RegisterModel(), ops) + res := porcupine.CheckOperations(convertModel(RegisterModel()), ops) if res != true { t.Fatal("expected operations to be linearizable") } @@ -31,7 +31,7 @@ func TestRegisterModel(t *testing.T) { {porcupine.ReturnEvent, RegisterResponse{false, 100}, 1}, {porcupine.ReturnEvent, RegisterResponse{false, 0}, 0}, } - res = porcupine.CheckEvents(RegisterModel(), events) + res = porcupine.CheckEvents(convertModel(RegisterModel()), events) if res != true { t.Fatal("expected operations to be linearizable") } @@ -41,7 +41,7 @@ func TestRegisterModel(t *testing.T) { {RegisterRequest{RegisterRead, 0}, 10, RegisterResponse{false, 200}, 30}, {RegisterRequest{RegisterRead, 0}, 40, RegisterResponse{false, 0}, 90}, } - res = porcupine.CheckOperations(RegisterModel(), ops) + res = porcupine.CheckOperations(convertModel(RegisterModel()), ops) if res != false { t.Fatal("expected operations to not be linearizable") } @@ -55,7 +55,7 @@ func TestRegisterModel(t *testing.T) { {porcupine.ReturnEvent, RegisterResponse{false, 0}, 2}, {porcupine.ReturnEvent, RegisterResponse{false, 0}, 0}, } - res = porcupine.CheckEvents(RegisterModel(), events) + res = porcupine.CheckEvents(convertModel(RegisterModel()), events) if res != false { t.Fatal("expected operations to not be linearizable") }