diff --git a/.github/workflows/packer.yml b/.github/workflows/packer.yml index 308ddbdbe..7a60388b5 100644 --- a/.github/workflows/packer.yml +++ b/.github/workflows/packer.yml @@ -71,6 +71,9 @@ jobs: working-directory: provisioning/packer steps: - uses: actions/checkout@v2 + - name: Get source tag of git + id: get_source_tag + run: echo ::set-output name=SOURCE_TAG::${GITHUB_REF#refs/tags/} - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 @@ -95,19 +98,14 @@ jobs: - run: make clean-output - name: "make build-contestant" + env: + GIT_TAG: ${{ steps.get_source_tag.outputs.SOURCE_TAG }} run: | export PATH=${HOME}/work/_tool/isucon11-qualify:${PATH} make build-contestant - name: "make build-bench" + env: + GIT_TAG: ${{ steps.get_source_tag.outputs.SOURCE_TAG }} run: | export PATH=${HOME}/work/_tool/isucon11-qualify:${PATH} make build-bench - - - uses: actions/upload-artifact@v2 - with: - name: manifest-amd64-contestant.json - path: provisioning/packer/output/manifest-amd64-contestant.json - - uses: actions/upload-artifact@v2 - with: - name: manifest-amd64-bench.json - path: provisioning/packer/output/manifest-amd64-bench.json diff --git a/bench/main.go b/bench/main.go index 700b4e2de..b7ef8db17 100644 --- a/bench/main.go +++ b/bench/main.go @@ -245,7 +245,7 @@ func main() { critical, _, deduction := checkError(err) - if critical || (deduction && atomic.AddInt64(&errorCount, 1) >= FAIL_ERROR_COUNT) { + if critical || (deduction && atomic.AddInt64(&errorCount, 1) > FAIL_ERROR_COUNT) { step.Cancel() } diff --git a/bench/model/isu.go b/bench/model/isu.go index 3c84bf5e0..697acd386 100644 --- a/bench/model/isu.go +++ b/bench/model/isu.go @@ -1,6 +1,9 @@ package model -import "fmt" +import ( + "context" + "fmt" +) //enum type IsuStateChange int @@ -21,7 +24,7 @@ const ( type StreamsForPoster struct { ActiveChan chan<- bool StateChan <-chan IsuStateChange - ConditionChan chan<- *IsuCondition + ConditionChan chan<- []IsuCondition } //posterスレッドとシナリオスレッドとの通信に必要な情報 @@ -30,7 +33,7 @@ type StreamsForPoster struct { type StreamsForScenario struct { activeChan <-chan bool StateChan chan<- IsuStateChange - ConditionChan <-chan *IsuCondition + ConditionChan <-chan []IsuCondition } //一つのIsuにつき、一つの送信用スレッドがある @@ -45,7 +48,7 @@ type Isu struct { IsWantDeactivated bool //シナリオ上でDeleteリクエストを送ったかどうか isDeactivated bool //実際にdeactivateされているか StreamsForScenario *StreamsForScenario //posterスレッドとの通信 - Conditions []IsuCondition //シナリオスレッドからのみ参照 + Conditions IsuConditionArray //シナリオスレッドからのみ参照 } //新しいISUの生成 @@ -56,7 +59,7 @@ type Isu struct { func NewRandomIsuRaw(owner *User) (*Isu, *StreamsForPoster, error) { activeChan := make(chan bool) stateChan := make(chan IsuStateChange, 1) - conditionChan := make(chan *IsuCondition, 30) + conditionChan := make(chan []IsuCondition, 10) id := fmt.Sprintf("randomid-%s-%d", owner.UserID, len(owner.IsuListOrderByCreatedAt)) //TODO: ちゃんと生成する name := fmt.Sprintf("randomname-%s-%d", owner.UserID, len(owner.IsuListOrderByCreatedAt)) //TODO: ちゃんと生成する @@ -74,7 +77,7 @@ func NewRandomIsuRaw(owner *User) (*Isu, *StreamsForPoster, error) { StateChan: stateChan, ConditionChan: conditionChan, }, - Conditions: []IsuCondition{}, + Conditions: NewIsuConditionArray(), } streamsForPoster := &StreamsForPoster{ @@ -94,3 +97,26 @@ func (isu *Isu) IsDeactivated() bool { } return isu.isDeactivated } + +func (isu *Isu) getConditionFromChan(ctx context.Context, userConditionBuffer *IsuConditionArray) { + for { + select { + case <-ctx.Done(): + return + case conditions, ok := <-isu.StreamsForScenario.ConditionChan: + if !ok { + return + } + for _, c := range conditions { + isu.Conditions.Add(&c) + } + if userConditionBuffer != nil { + for _, c := range conditions { + userConditionBuffer.Add(&c) + } + } + default: + return + } + } +} diff --git a/bench/model/isuCondition.go b/bench/model/isuCondition.go index 2bf0c6377..dcd77d27d 100644 --- a/bench/model/isuCondition.go +++ b/bench/model/isuCondition.go @@ -4,9 +4,10 @@ package model type ConditionLevel int const ( - ConditionLevelInfo ConditionLevel = iota - ConditionLevelWarning - ConditionLevelCritical + ConditionLevelNone ConditionLevel = 0 + ConditionLevelInfo ConditionLevel = 1 + ConditionLevelWarning ConditionLevel = 2 + ConditionLevelCritical ConditionLevel = 4 ) //TODO: メモリ節約の必要があるなら考える @@ -20,5 +21,215 @@ type IsuCondition struct { IsBroken bool ConditionLevel ConditionLevel `json:"-"` Message string `json:"message"` + OwnerID string // Owner *Isu } + +//left < right +func (left *IsuCondition) Less(right *IsuCondition) bool { + return left.TimestampUnix < right.TimestampUnix || + (left.TimestampUnix == right.TimestampUnix && left.OwnerID < right.OwnerID) +} + +type IsuConditionCursor struct { + TimestampUnix int64 + OwnerID string +} + +//left < right +func (left *IsuConditionCursor) Less(right *IsuConditionCursor) bool { + return left.TimestampUnix < right.TimestampUnix || + (left.TimestampUnix == right.TimestampUnix && left.OwnerID < right.OwnerID) +} + +//left < right +func (left *IsuCondition) Less2(right *IsuConditionCursor) bool { + return left.TimestampUnix < right.TimestampUnix || + (left.TimestampUnix == right.TimestampUnix && left.OwnerID < right.OwnerID) +} + +//left < right +func (left *IsuConditionCursor) Less2(right *IsuCondition) bool { + return left.TimestampUnix < right.TimestampUnix || + (left.TimestampUnix == right.TimestampUnix && left.OwnerID < right.OwnerID) +} + +//conditionをcreated at順で見る +type IsuConditionArray struct { + Info []IsuCondition + Warning []IsuCondition + Critical []IsuCondition +} + +//conditionをcreated atの大きい順で見る +type IsuConditionIterator struct { + filter ConditionLevel + indexInfo int + indexWarning int + indexCritical int + parent *IsuConditionArray +} + +func NewIsuConditionArray() IsuConditionArray { + return IsuConditionArray{ + Info: []IsuCondition{}, + Warning: []IsuCondition{}, + Critical: []IsuCondition{}, + } +} + +func (ia *IsuConditionArray) Add(cond *IsuCondition) { + switch cond.ConditionLevel { + case ConditionLevelInfo: + ia.Info = append(ia.Info, *cond) + case ConditionLevelWarning: + ia.Warning = append(ia.Warning, *cond) + case ConditionLevelCritical: + ia.Critical = append(ia.Critical, *cond) + } +} + +func (ia *IsuConditionArray) End(filter ConditionLevel) IsuConditionIterator { + return IsuConditionIterator{ + filter: filter, + indexInfo: len(ia.Info), + indexWarning: len(ia.Warning), + indexCritical: len(ia.Critical), + parent: ia, + } +} + +func (ia *IsuConditionArray) Back() *IsuCondition { + iter := ia.End(ConditionLevelInfo | ConditionLevelWarning | ConditionLevelCritical) + return iter.Prev() +} + +func (iter *IsuConditionIterator) UpperBoundIsuConditionIndex(targetTimestamp int64, targetIsuUUID string) { + if (iter.filter & ConditionLevelInfo) != 0 { + iter.indexInfo = upperBoundIsuConditionIndex(iter.parent.Info, len(iter.parent.Info), targetTimestamp, targetIsuUUID) + } + if (iter.filter & ConditionLevelWarning) != 0 { + iter.indexWarning = upperBoundIsuConditionIndex(iter.parent.Warning, len(iter.parent.Warning), targetTimestamp, targetIsuUUID) + } + if (iter.filter & ConditionLevelCritical) != 0 { + iter.indexCritical = upperBoundIsuConditionIndex(iter.parent.Critical, len(iter.parent.Critical), targetTimestamp, targetIsuUUID) + } +} + +func (iter *IsuConditionIterator) LowerBoundIsuConditionIndex(targetTimestamp int64, targetIsuUUID string) { + if (iter.filter & ConditionLevelInfo) != 0 { + iter.indexInfo = lowerBoundIsuConditionIndex(iter.parent.Info, len(iter.parent.Info), targetTimestamp, targetIsuUUID) + } + if (iter.filter & ConditionLevelWarning) != 0 { + iter.indexWarning = lowerBoundIsuConditionIndex(iter.parent.Warning, len(iter.parent.Warning), targetTimestamp, targetIsuUUID) + } + if (iter.filter & ConditionLevelCritical) != 0 { + iter.indexCritical = lowerBoundIsuConditionIndex(iter.parent.Critical, len(iter.parent.Critical), targetTimestamp, targetIsuUUID) + } +} + +//return: nil:もう要素がない +func (iter *IsuConditionIterator) Prev() *IsuCondition { + maxType := ConditionLevelNone + var max *IsuCondition + if (iter.filter&ConditionLevelInfo) != 0 && iter.indexInfo != 0 { + if max == nil || max.Less(&iter.parent.Info[iter.indexInfo-1]) { + maxType = ConditionLevelInfo + max = &iter.parent.Info[iter.indexInfo-1] + } + } + if (iter.filter&ConditionLevelWarning) != 0 && iter.indexWarning != 0 { + if max == nil || max.Less(&iter.parent.Warning[iter.indexWarning-1]) { + maxType = ConditionLevelWarning + max = &iter.parent.Warning[iter.indexWarning-1] + } + } + if (iter.filter&ConditionLevelCritical) != 0 && iter.indexCritical != 0 { + if max == nil || max.Less(&iter.parent.Critical[iter.indexCritical-1]) { + maxType = ConditionLevelCritical + max = &iter.parent.Critical[iter.indexCritical-1] + } + } + + switch maxType { + case ConditionLevelInfo: + iter.indexInfo-- + case ConditionLevelWarning: + iter.indexWarning-- + case ConditionLevelCritical: + iter.indexCritical-- + } + return max +} + +//baseはlessの昇順 +func upperBoundIsuConditionIndex(base []IsuCondition, end int, targetTimestamp int64, targetIsuUUID string) int { + //末尾の方にあることが分かっているので、末尾を固定要素ずつ線形探索 + 二分探索 + //assert end <= len(base) + target := IsuConditionCursor{TimestampUnix: targetTimestamp, OwnerID: targetIsuUUID} + if end <= 0 { + return end //要素が見つからない + } + //[0]が番兵になるかチェック + if target.Less2(&base[0]) { + return 0 //0がupperBound + } + + //線形探索 ngがbase[ng] <= targetになるまで探索 + const defaultRange = 64 + ok := end + ng := end - defaultRange + ng = (ng / defaultRange) * defaultRange //0未満になるのが嫌なので、defaultRangeの倍数にする + for target.Less2(&base[ng]) { //Timestampはunique仮定なので、<で良い(等価が見つかればそれで良し) + ok = ng + ng -= defaultRange + } + + //答えは(ng, ok]内にあるはずなので、二分探索 + for ok-ng > 1 { + mid := (ok + ng) / 2 + if target.Less2(&base[mid]) { + ok = mid + } else { + ng = mid + } + } + + return ok +} + +//baseはlessの昇順 +func lowerBoundIsuConditionIndex(base []IsuCondition, end int, targetTimestamp int64, targetIsuUUID string) int { + //末尾の方にあることが分かっているので、末尾を固定要素ずつ線形探索 + 二分探索 + //assert end <= len(base) + target := IsuConditionCursor{TimestampUnix: targetTimestamp, OwnerID: targetIsuUUID} + if end <= 0 { + return end //要素が見つからない + } + //[0]が番兵になるかチェック + if !base[0].Less2(&target) { + return 0 //0がupperBound + } + + //線形探索 ngがbase[ng] <= targetになるまで探索 + const defaultRange = 64 + ok := end + ng := end - defaultRange + ng = (ng / defaultRange) * defaultRange //0未満になるのが嫌なので、defaultRangeの倍数にする + for !base[ng].Less2(&target) { //Timestampはunique仮定なので、<で良い(等価が見つかればそれで良し) + ok = ng + ng -= defaultRange + } + + //答えは(ng, ok]内にあるはずなので、二分探索 + for ok-ng > 1 { + mid := (ok + ng) / 2 + if !base[mid].Less2(&target) { + ok = mid + } else { + ng = mid + } + } + + return ok +} diff --git a/bench/model/user.go b/bench/model/user.go index 9c9f2fd89..8a2cb0960 100644 --- a/bench/model/user.go +++ b/bench/model/user.go @@ -1,6 +1,7 @@ package model import ( + "context" "math/rand" "github.com/isucon/isucandar/agent" @@ -23,7 +24,7 @@ type User struct { Type UserType IsuListOrderByCreatedAt []*Isu //CreatedAtは厳密にはわからないので、postした後にgetをした順番を正とする IsuListByID map[string]*Isu //IDをkeyにアクセス - //ここで[]IsuLogを持つと更新にmutexが必要で嫌なので持たない + Conditions IsuConditionArray Agent *agent.Agent } @@ -48,6 +49,12 @@ func (u *User) AddIsu(isu *Isu) { u.IsuListByID[isu.JIAIsuUUID] = isu } +func (user *User) GetConditionFromChan(ctx context.Context) { + for _, isu := range user.IsuListOrderByCreatedAt { + isu.getConditionFromChan(ctx, &user.Conditions) + } +} + // utility //TODO: 差し替える diff --git a/bench/scenario/action.go b/bench/scenario/action.go index 599ece66e..6b1bdcc52 100644 --- a/bench/scenario/action.go +++ b/bench/scenario/action.go @@ -29,6 +29,12 @@ import ( "github.com/isucon/isucon11-qualify/bench/service" ) +const ( + searchLimit = 20 + conditionLimit = 20 + isuListLimit = 200 // TODO 修正が必要なら変更 +) + //Action // ==============================initialize============================== @@ -519,7 +525,7 @@ func postIsuConditionErrorAction(ctx context.Context, a *agent.Agent, id string, } func getIsuConditionAction(ctx context.Context, a *agent.Agent, id string, req service.GetIsuConditionRequest) ([]*service.GetIsuConditionResponse, *http.Response, error) { - reqUrl := getIsuConditionRequestParams(fmt.Sprintf("/api/condition/%s?", id), req) + reqUrl := getIsuConditionRequestParams(fmt.Sprintf("/api/condition/%s", id), req) conditions := []*service.GetIsuConditionResponse{} res, err := reqJSONResJSON(ctx, a, http.MethodGet, reqUrl, nil, &conditions, []int{http.StatusOK}) if err != nil { @@ -529,7 +535,7 @@ func getIsuConditionAction(ctx context.Context, a *agent.Agent, id string, req s } func getIsuConditionErrorAction(ctx context.Context, a *agent.Agent, id string, req service.GetIsuConditionRequest) (string, *http.Response, error) { - reqUrl := getIsuConditionRequestParams(fmt.Sprintf("/api/condition/%s?", id), req) + reqUrl := getIsuConditionRequestParams(fmt.Sprintf("/api/condition/%s", id), req) res, text, err := reqNoContentResError(ctx, a, http.MethodGet, reqUrl, []int{http.StatusNotFound, http.StatusUnauthorized}) if err != nil { return "", nil, err @@ -538,7 +544,7 @@ func getIsuConditionErrorAction(ctx context.Context, a *agent.Agent, id string, } func getConditionAction(ctx context.Context, a *agent.Agent, req service.GetIsuConditionRequest) ([]*service.GetIsuConditionResponse, *http.Response, error) { - reqUrl := getIsuConditionRequestParams("/api/condition?", req) + reqUrl := getIsuConditionRequestParams("/api/condition", req) conditions := []*service.GetIsuConditionResponse{} res, err := reqJSONResJSON(ctx, a, http.MethodGet, reqUrl, nil, &conditions, []int{http.StatusOK}) if err != nil { @@ -548,7 +554,7 @@ func getConditionAction(ctx context.Context, a *agent.Agent, req service.GetIsuC } func getConditionErrorAction(ctx context.Context, a *agent.Agent, req service.GetIsuConditionRequest) (string, *http.Response, error) { - reqUrl := getIsuConditionRequestParams("/api/condition?", req) + reqUrl := getIsuConditionRequestParams("/api/condition", req) res, text, err := reqNoContentResError(ctx, a, http.MethodGet, reqUrl, []int{http.StatusNotFound, http.StatusUnauthorized}) if err != nil { return "", nil, err @@ -585,6 +591,9 @@ func getIsuGraphAction(ctx context.Context, a *agent.Agent, id string, date uint if err != nil { return nil, nil, err } + + //TODO: バリデーション + return graph, res, nil } @@ -598,6 +607,7 @@ func getIsuGraphErrorAction(ctx context.Context, a *agent.Agent, id string, date } func browserGetHomeAction(ctx context.Context, a *agent.Agent, + virtualNowUnix int64, validateIsu func(*http.Response, []*service.Isu) []error, validateCondition func(*http.Response, []*service.GetIsuConditionResponse) []error, ) ([]*service.Isu, []*service.GetIsuConditionResponse, []error) { @@ -621,7 +631,7 @@ func browserGetHomeAction(ctx context.Context, a *agent.Agent, errors = append(errors, validateIsu(hres, isuList)...) } - conditions, hres, err := getConditionAction(ctx, a, service.GetIsuConditionRequest{CursorEndTime: uint64(time.Now().Unix()), CursorJIAIsuUUID: "z", ConditionLevel: "critical,warning,info"}) + conditions, hres, err := getConditionAction(ctx, a, service.GetIsuConditionRequest{CursorEndTime: uint64(virtualNowUnix), CursorJIAIsuUUID: "z", ConditionLevel: "critical,warning,info"}) if err != nil { errors = append(errors, err) } else { @@ -727,7 +737,9 @@ func browserGetIsuConditionAction(ctx context.Context, a *agent.Agent, id string return isu, conditions, errors } -func browserGetIsuGraph(ctx context.Context, a *agent.Agent, id string, date uint64) (*service.Isu, []*service.GraphResponse, []error) { +func browserGetIsuGraphAction(ctx context.Context, a *agent.Agent, id string, date uint64, + validateGraph func(*http.Response, []*service.GraphResponse) []error, +) (*service.Isu, []*service.GraphResponse, []error) { // TODO: 静的ファイルのGET errors := []error{} @@ -736,9 +748,11 @@ func browserGetIsuGraph(ctx context.Context, a *agent.Agent, id string, date uin if err != nil { errors = append(errors, err) } - graph, _, err := getIsuGraphAction(ctx, a, id, date) + graph, res, err := getIsuGraphAction(ctx, a, id, date) if err != nil { errors = append(errors, err) + } else { + errors = append(errors, validateGraph(res, graph)...) } return isu, graph, errors } diff --git a/bench/scenario/error.go b/bench/scenario/error.go index fd4417478..58097f195 100644 --- a/bench/scenario/error.go +++ b/bench/scenario/error.go @@ -119,6 +119,11 @@ func errorMissmatch(res *http.Response, message string, args ...interface{}) err return failure.NewError(ErrMissmatch, fmt.Errorf(message+": %d (%s: %s)", args...)) } +func errorInvalid(res *http.Response, message string, args ...interface{}) error { + args = append(args, res.StatusCode, res.Request.Method, res.Request.URL.Path) + return failure.NewError(ErrInvalid, fmt.Errorf(message+": %d (%s: %s)", args...)) +} + func errorBadResponse(res *http.Response, message string, args ...interface{}) error { args = append(args, res.StatusCode, res.Request.Method, res.Request.URL.Path) return failure.NewError(ErrBadResponse, fmt.Errorf(message+": %d (%s: %s)", args...)) diff --git a/bench/scenario/load.go b/bench/scenario/load.go index b86cb77cc..c3fad68d0 100644 --- a/bench/scenario/load.go +++ b/bench/scenario/load.go @@ -89,6 +89,10 @@ func (s *Scenario) loadNormalUser(ctx context.Context, step *isucandar.Benchmark nextTargetIsuIndex := 0 scenarioDoneCount := 0 scenarioSuccess := false + lastSolvedTime := make(map[string]time.Time) + for _, isu := range user.IsuListOrderByCreatedAt { + lastSolvedTime[isu.JIAIsuUUID] = s.virtualTimeStart + } scenarioLoop: for { select { @@ -102,42 +106,35 @@ scenarioLoop: step.AddScore(ScoreNormalUserLoop) //TODO: 得点条件の修正 //シナリオに成功している場合は椅子追加 - if isuCount < scenarioDoneCount/30 { + if isuCount < scenarioDoneCount/30 && isuCount < isuCountMax { isu := s.NewIsu(ctx, step, user, true) if isu == nil { logger.AdminLogger.Println("Normal User fail: NewIsu") } else { isuCount++ } + //logger.AdminLogger.Printf("Normal User Isu: %d\n", isuCount) } } scenarioSuccess = true //posterからconditionの取得 - for _, isu := range user.IsuListOrderByCreatedAt { - getConditionFromPosterLoop: - for { - select { - case <-ctx.Done(): - return - case cond, ok := <-isu.StreamsForScenario.ConditionChan: - if !ok { - break getConditionFromPosterLoop - } - isu.Conditions = append(isu.Conditions, *cond) - default: - break getConditionFromPosterLoop - } - } + user.GetConditionFromChan(ctx) + select { + case <-ctx.Done(): + return + default: } //TODO: 乱数にする nextTargetIsuIndex += 1 nextTargetIsuIndex %= isuCount targetIsu := user.IsuListOrderByCreatedAt[nextTargetIsuIndex] + mustExistUntil := s.ToVirtualTime(time.Now().Add(-1 * time.Second)).Unix() //GET / - _, _, errs := browserGetHomeAction(ctx, user.Agent, + dataExistTimestamp := GetConditionDataExistTimestamp(s, user) + _, _, errs := browserGetHomeAction(ctx, user.Agent, dataExistTimestamp, func(res *http.Response, isuList []*service.Isu) []error { return verifyIsuOrderByCreatedAt(res, user.IsuListOrderByCreatedAt, isuList) }, @@ -169,16 +166,23 @@ scenarioLoop: //TODO: リロード //定期的にconditionを見に行くシナリオ - virtualNow := s.ToVirtualTime(time.Now()) + request := service.GetIsuConditionRequest{ + StartTime: nil, + CursorEndTime: uint64(dataExistTimestamp), + CursorJIAIsuUUID: "", + ConditionLevel: "info,warning,critical", + Limit: nil, + } _, conditions, errs := browserGetIsuConditionAction(ctx, user.Agent, targetIsu.JIAIsuUUID, - service.GetIsuConditionRequest{ - StartTime: nil, - CursorEndTime: uint64(virtualNow.Unix()), - CursorJIAIsuUUID: "", - ConditionLevel: "info,warning,critical", - Limit: nil, - }, + request, func(res *http.Response, conditions []*service.GetIsuConditionResponse) []error { + //conditionの検証 + err := verifyIsuConditions(res, user, targetIsu.JIAIsuUUID, &request, + conditions, mustExistUntil, + ) + if err != nil { + return []error{err} + } return []error{} }, ) @@ -186,68 +190,173 @@ scenarioLoop: scenarioSuccess = false step.AddError(err) } - if len(errs) > 0 { + if len(errs) > 0 || len(conditions) == 0 { continue scenarioLoop } //スクロール - var res *http.Response for i := 0; i < 2 && len(conditions) == 20*(i+1); i++ { var conditionsTmp []*service.GetIsuConditionResponse - conditionsTmp, res, err = getIsuConditionAction(ctx, user.Agent, targetIsu.JIAIsuUUID, - service.GetIsuConditionRequest{ - StartTime: nil, - CursorEndTime: uint64(conditions[len(conditions)-1].Timestamp), - CursorJIAIsuUUID: "", - ConditionLevel: "info,warning,critical", - Limit: nil, - }, + CursorEndTime := conditions[len(conditions)-1].Timestamp + request = service.GetIsuConditionRequest{ + StartTime: nil, + CursorEndTime: uint64(CursorEndTime), + CursorJIAIsuUUID: "", + ConditionLevel: "info,warning,critical", + Limit: nil, + } + conditionsTmp, res, err := getIsuConditionAction(ctx, user.Agent, targetIsu.JIAIsuUUID, request) + if err != nil { + scenarioSuccess = false + step.AddError(err) + break + } + //検証 + //ここは、古いデータのはずなのでconditionのchanからの再取得は要らない + err = verifyIsuConditions(res, user, targetIsu.JIAIsuUUID, &request, + conditionsTmp, mustExistUntil, ) if err != nil { scenarioSuccess = false step.AddError(err) break - } else { - conditions = append(conditions, conditionsTmp...) } - } - //TODO: conditionの検証 - if res != nil { //エラーつぶし + conditions = append(conditions, conditionsTmp...) } //conditionを確認して、椅子状態を改善 - //TODO: すでに改善済みのものを弾く - solvedCondition := model.IsuStateChangeNone - for _, c := range conditions { - //MEMO: 重かったらフォーマットが想定通りの前提で最適化する - for _, cond := range strings.Split(c.Condition, ",") { - keyValue := strings.Split(cond, "=") - if len(keyValue) != 2 { - continue //形式に従っていないものは無視 - } - if keyValue[1] != "false" { - if keyValue[0] == "is_dirty" { - solvedCondition |= model.IsuStateChangeClear - } else if keyValue[0] == "is_overweight" { - solvedCondition |= model.IsuStateChangeDetectOverweight - } else if keyValue[0] == "is_broken" { - solvedCondition |= model.IsuStateChangeRepair - } - } + solvedCondition, findTimestamp := findBadIsuState(conditions) + if solvedCondition != model.IsuStateChangeNone && lastSolvedTime[targetIsu.JIAIsuUUID].Before(time.Unix(findTimestamp, 0)) { + //graphを見る + virtualDay := (findTimestamp / (24 * 60 * 60)) * (24 * 60 * 60) + _, _, errs := browserGetIsuGraphAction(ctx, user.Agent, targetIsu.JIAIsuUUID, uint64(virtualDay), + func(res *http.Response, graph []*service.GraphResponse) []error { + return []error{} //TODO: 検証 + }, + ) + for _, err := range errs { + scenarioSuccess = false + step.AddError(err) } - } - if solvedCondition != model.IsuStateChangeNone { - //TODO: graph - - go func() { targetIsu.StreamsForScenario.StateChan <- solvedCondition }() + //状態改善 + lastSolvedTime[targetIsu.JIAIsuUUID] = time.Unix(findTimestamp, 0) + targetIsu.StreamsForScenario.StateChan <- solvedCondition //バッファがあるのでブロック率は低い読みで直列に投げる } } else { //TODO: graphを見に行くシナリオ + virtualToday := (dataExistTimestamp / (24 * 60 * 60)) * (24 * 60 * 60) + _, graphToday, errs := browserGetIsuGraphAction(ctx, user.Agent, targetIsu.JIAIsuUUID, uint64(virtualToday), + func(res *http.Response, graph []*service.GraphResponse) []error { + //検証前にデータ取得 + user.GetConditionFromChan(ctx) + return []error{} //TODO: 検証 + }, + ) + for _, err := range errs { + scenarioSuccess = false + step.AddError(err) + } + if len(errs) > 0 { + continue scenarioLoop + } + + //前日のグラフ + _, _, errs = browserGetIsuGraphAction(ctx, user.Agent, targetIsu.JIAIsuUUID, uint64(virtualToday-60*60), + func(res *http.Response, graph []*service.GraphResponse) []error { + return []error{} //TODO: 検証 + }, + ) + for _, err := range errs { + scenarioSuccess = false + step.AddError(err) + } + if len(errs) > 0 { + continue scenarioLoop + } + + //悪いものを探す + var errorEndAtUnix int64 = 0 + for _, g := range graphToday { + if g.Data != nil && g.Data.Score < 100 { + errorEndAtUnix = g.StartAt + } + } + + //悪いものがあれば、そのconditionを取る + if errorEndAtUnix != 0 { + startTime := uint64(errorEndAtUnix - 60*60) + request := service.GetIsuConditionRequest{ + StartTime: &startTime, + CursorEndTime: uint64(errorEndAtUnix), + CursorJIAIsuUUID: "", + ConditionLevel: "warning,critical", + Limit: nil, + } + _, conditions, errs := browserGetIsuConditionAction(ctx, user.Agent, targetIsu.JIAIsuUUID, + request, + func(res *http.Response, conditions []*service.GetIsuConditionResponse) []error { + //検証 + //ここは、古いデータのはずなのでconditionのchanからの再取得は要らない + //TODO: starttimeの検証 + err := verifyIsuConditions(res, user, targetIsu.JIAIsuUUID, &request, + conditions, mustExistUntil, + ) + if err != nil { + return []error{err} + } + return []error{} + }, + ) + for _, err := range errs { + scenarioSuccess = false + step.AddError(err) + } + if len(errs) > 0 { + continue scenarioLoop + } + + //状態改善 + solvedCondition, findTimestamp := findBadIsuState(conditions) + if solvedCondition != model.IsuStateChangeNone && lastSolvedTime[targetIsu.JIAIsuUUID].Before(time.Unix(findTimestamp, 0)) { + lastSolvedTime[targetIsu.JIAIsuUUID] = time.Unix(findTimestamp, 0) + targetIsu.StreamsForScenario.StateChan <- solvedCondition //バッファがあるのでブロック率は低い読みで直列に投げる + } + } } + } +} - //TODO: 椅子の追加 +func findBadIsuState(conditions []*service.GetIsuConditionResponse) (model.IsuStateChange, int64) { + //TODO: すでに改善済みのものを弾く + + var virtualTimestamp int64 + solvedCondition := model.IsuStateChangeNone + for _, c := range conditions { + //MEMO: 重かったらフォーマットが想定通りの前提で最適化する + bad := false + for _, cond := range strings.Split(c.Condition, ",") { + keyValue := strings.Split(cond, "=") + if len(keyValue) != 2 { + continue //形式に従っていないものは無視 + } + if keyValue[1] != "false" { + bad = true + if keyValue[0] == "is_dirty" { + solvedCondition |= model.IsuStateChangeClear + } else if keyValue[0] == "is_overweight" { + solvedCondition |= model.IsuStateChangeDetectOverweight + } else if keyValue[0] == "is_broken" { + solvedCondition |= model.IsuStateChangeRepair + } + } + } + if bad && virtualTimestamp == 0 { + virtualTimestamp = c.Timestamp + } } + + return solvedCondition, virtualTimestamp } diff --git a/bench/scenario/posting.go b/bench/scenario/posting.go index 7342ee935..52822bc26 100644 --- a/bench/scenario/posting.go +++ b/bench/scenario/posting.go @@ -75,13 +75,13 @@ func (s *Scenario) keepPosting(ctx context.Context, step *isucandar.BenchmarkSte //TODO: 検証可能な生成方法にする //TODO: stateの適用タイミングをちゃんと考える - conditions := []*model.IsuCondition{} + conditions := []model.IsuCondition{} conditionsReq := []service.PostIsuConditionRequest{} conditionLevelWorst := model.ConditionLevelInfo for state.NextConditionTimestamp().Before(nowTimeStamp) { //次のstateを生成 - condition := state.GenerateNextCondition(randEngine, stateChange) //TODO: stateの適用タイミングをちゃんと考える - stateChange = model.IsuStateChangeNone //TODO: stateの適用タイミングをちゃんと考える + condition := state.GenerateNextCondition(randEngine, stateChange, jiaIsuUUID) //TODO: stateの適用タイミングをちゃんと考える + stateChange = model.IsuStateChangeNone //TODO: stateの適用タイミングをちゃんと考える if conditionLevelWorst < condition.ConditionLevel { conditionLevelWorst = condition.ConditionLevel } @@ -134,9 +134,7 @@ func (s *Scenario) keepPosting(ctx context.Context, step *isucandar.BenchmarkSte step.AddScore(ScorePostConditionCritical) } go func() { - for _, c := range conditions { - scenarioChan.ConditionChan <- c - } + scenarioChan.ConditionChan <- conditions }() }() } @@ -148,7 +146,7 @@ func (state *posterState) NextConditionTimestamp() time.Time { func (state *posterState) NextIsLatestTimestamp(nowTimeStamp time.Time) bool { return nowTimeStamp.Before(time.Unix(state.lastCondition.TimestampUnix, 0).Add(PostInterval * 2)) } -func (state *posterState) GenerateNextCondition(randEngine *rand.Rand, stateChange model.IsuStateChange) *model.IsuCondition { +func (state *posterState) GenerateNextCondition(randEngine *rand.Rand, stateChange model.IsuStateChange, jiaIsuUUID string) model.IsuCondition { //乱数初期化(逆算できるように) timeStamp := state.NextConditionTimestamp() @@ -186,10 +184,10 @@ func (state *posterState) GenerateNextCondition(randEngine *rand.Rand, stateChan } //新しいConditionを生成 - var condition *model.IsuCondition + var condition model.IsuCondition if state.isuStateDelete { //削除された椅子のConditionは0点固定 - condition = &model.IsuCondition{ + condition = model.IsuCondition{ StateChange: model.IsuStateChangeDelete, IsSitting: true, IsDirty: true, @@ -198,10 +196,11 @@ func (state *posterState) GenerateNextCondition(randEngine *rand.Rand, stateChan ConditionLevel: model.ConditionLevelCritical, Message: "", TimestampUnix: timeStamp.Unix(), + OwnerID: jiaIsuUUID, } } else { //新しいConditionを生成 - condition = &model.IsuCondition{ + condition = model.IsuCondition{ StateChange: stateChange, IsSitting: state.lastCondition.IsSitting, IsDirty: lastConditionIsDirty, @@ -210,6 +209,7 @@ func (state *posterState) GenerateNextCondition(randEngine *rand.Rand, stateChan //ConditionLevel: model.ConditionLevelCritical, Message: "", TimestampUnix: timeStamp.Unix(), + OwnerID: jiaIsuUUID, } //sitting if condition.IsSitting { @@ -263,7 +263,7 @@ func (state *posterState) GenerateNextCondition(randEngine *rand.Rand, stateChan } //last更新 - state.lastCondition = *condition + state.lastCondition = condition return condition } diff --git a/bench/scenario/scenario.go b/bench/scenario/scenario.go index 130e712ea..93df72831 100644 --- a/bench/scenario/scenario.go +++ b/bench/scenario/scenario.go @@ -2,6 +2,7 @@ package scenario import ( "context" + "math" "sync" "time" @@ -157,3 +158,20 @@ func (s *Scenario) NewIsu(ctx context.Context, step *isucandar.BenchmarkStep, ow return isu } + +func GetConditionDataExistTimestamp(s *Scenario, user *model.User) int64 { + if len(user.IsuListOrderByCreatedAt) == 0 { + return s.virtualTimeStart.Unix() + } + var timestamp int64 = math.MaxInt64 + for _, isu := range user.IsuListOrderByCreatedAt { + cond := isu.Conditions.Back() + if cond == nil { + return s.virtualTimeStart.Unix() + } + if cond.TimestampUnix < timestamp { + timestamp = cond.TimestampUnix + } + } + return timestamp +} diff --git a/bench/scenario/verify.go b/bench/scenario/verify.go index ba3a36243..985a07867 100644 --- a/bench/scenario/verify.go +++ b/bench/scenario/verify.go @@ -7,6 +7,7 @@ package scenario import ( "encoding/json" + "fmt" "net/http" "strings" @@ -110,3 +111,115 @@ func verifyIsuOrderByCreatedAt(res *http.Response, expectedReverse []*model.Isu, // errs := []error{} // return errs // } + +// +//mustExistUntil: この値以下のtimestampを持つものは全て反映されているべき +func verifyIsuConditions(res *http.Response, + targetUser *model.User, targetIsuUUID string, request *service.GetIsuConditionRequest, + backendData []*service.GetIsuConditionResponse, mustExistUntil int64) error { + + //limitを超えているかチェック + var limit int + if request.Limit != nil { + limit = int(*request.Limit) + } else { + limit = conditionLimit + } + if limit < len(backendData) { + return errorInvalid(res, "要素数が正しくありません") + } + //レスポンス側のstartTimeのチェック + if request.StartTime != nil && len(backendData) != 0 && backendData[len(backendData)-1].Timestamp < int64(*request.StartTime) { + return errorInvalid(res, "データが正しくありません") + } + + //expectedの開始位置を探す + filter := model.ConditionLevelNone + for _, level := range strings.Split(request.ConditionLevel, ",") { + switch level[0] { + case 'i': + filter |= model.ConditionLevelInfo + case 'w': + filter |= model.ConditionLevelWarning + case 'c': + filter |= model.ConditionLevelCritical + } + } + targetIsu := targetUser.IsuListByID[targetIsuUUID] + targetConditions := &targetIsu.Conditions + baseIter := targetConditions.End(filter) + baseIter.LowerBoundIsuConditionIndex(int64(request.CursorEndTime), request.CursorJIAIsuUUID) + + //backendDataの先頭からチェック + var lastSort model.IsuConditionCursor + for i, c := range backendData { + nowSort := model.IsuConditionCursor{TimestampUnix: c.Timestamp, OwnerID: c.JIAIsuUUID} + if i != 0 && !nowSort.Less(&lastSort) { + return errorInvalid(res, "整列順が正しくありません") + } + + var expected *model.IsuCondition + for { + expected = baseIter.Prev() + if expected == nil { + return errorMissmatch(res, "存在しないはずのデータが返されました") + } + + if expected.TimestampUnix == c.Timestamp { + break //ok + } + + if mustExistUntil < expected.TimestampUnix { + //反映されていないことが許可されているので、無視して良い + continue + } + return errorMissmatch(res, "データが正しくありません") + } + + //等価チェック + expectedCondition := fmt.Sprintf("is_dirty=%v,is_overweight=%v,is_broken=%v", + expected.IsDirty, + expected.IsOverweight, + expected.IsBroken, + ) + var expectedConditionLevelStr string + warnCount := 0 + if expected.IsDirty { + warnCount++ + } + if expected.IsOverweight { + warnCount++ + } + if expected.IsBroken { + warnCount++ + } + switch warnCount { + case 0: + expectedConditionLevelStr = "info" + case 1, 2: + expectedConditionLevelStr = "warning" + case 3: + expectedConditionLevelStr = "critical" + } + if c.Condition != expectedCondition || + c.ConditionLevel != expectedConditionLevelStr || + c.IsSitting != expected.IsSitting || + c.JIAIsuUUID != expected.OwnerID || + c.Message != expected.Message || + c.IsuName != targetIsu.Name { + return errorMissmatch(res, "データが正しくありません") + } + + lastSort = nowSort + } + + //limitの検証 + if len(backendData) < limit && baseIter.Prev() != nil { + prev := baseIter.Prev() + if prev != nil && request.StartTime != nil && int64(*request.StartTime) <= prev.TimestampUnix { + return errorInvalid(res, "要素数が正しくありません") + } + } + + return nil +} diff --git a/extra/jiaapi/cmd/standalone/main.go b/extra/jiaapi/cmd/standalone/main.go index 977650c3f..cf461eaef 100644 --- a/extra/jiaapi/cmd/standalone/main.go +++ b/extra/jiaapi/cmd/standalone/main.go @@ -331,7 +331,7 @@ func (state *IsuConditionPoster) keepPosting(ctx context.Context) { nowTime := time.Now() randEngine.Seed(nowTime.UnixNano()/1000000000 + 961054102) - notification, err := json.Marshal(IsuNotification{ + notification := IsuNotification{ IsSitting: (randEngine.Intn(100) <= 70), Condition: fmt.Sprintf("is_dirty=%v,is_overweight=%v,is_broken=%v", (randEngine.Intn(2) == 0), @@ -340,7 +340,9 @@ func (state *IsuConditionPoster) keepPosting(ctx context.Context) { ), Message: "今日もいい天気", Timestamp: nowTime.Unix(), - }) + } + notifications := []IsuNotification{notification} + payload, err := json.Marshal(notifications) if err != nil { log.Error(err) continue @@ -349,7 +351,7 @@ func (state *IsuConditionPoster) keepPosting(ctx context.Context) { func() { resp, err := http.Post( targetURL, "application/json", - bytes.NewBuffer(notification), + bytes.NewBuffer(payload), ) if err != nil { log.Error(err) diff --git a/provisioning/ansible/roles/common/tasks/package.yml b/provisioning/ansible/roles/common/tasks/package.yml index b09628c3f..d273dc21b 100644 --- a/provisioning/ansible/roles/common/tasks/package.yml +++ b/provisioning/ansible/roles/common/tasks/package.yml @@ -22,7 +22,6 @@ - autoconf - automake - build-essential - - nginx - libxml2-dev - libsqlite3-dev - libbz2-dev diff --git a/provisioning/ansible/roles/contestant/files/etc/systemd/system/jiaapi_standalone.service b/provisioning/ansible/roles/contestant/files/etc/systemd/system/jiaapi-standalone.service similarity index 100% rename from provisioning/ansible/roles/contestant/files/etc/systemd/system/jiaapi_standalone.service rename to provisioning/ansible/roles/contestant/files/etc/systemd/system/jiaapi-standalone.service diff --git a/provisioning/ansible/roles/contestant/files/home/isucon/env.sh b/provisioning/ansible/roles/contestant/files/home/isucon/env.sh deleted file mode 100644 index 54760a70b..000000000 --- a/provisioning/ansible/roles/contestant/files/home/isucon/env.sh +++ /dev/null @@ -1,5 +0,0 @@ -MYSQL_HOST="127.0.0.1" -MYSQL_PORT=3306 -MYSQL_USER=isucon -MYSQL_DBNAME=isucondition -MYSQL_PASS=isucon diff --git a/provisioning/ansible/roles/contestant/files/var/lib/cloud/scripts/per-instance/generate-env_aws.sh b/provisioning/ansible/roles/contestant/files/var/lib/cloud/scripts/per-instance/generate-env_aws.sh new file mode 100644 index 000000000..3740b8edd --- /dev/null +++ b/provisioning/ansible/roles/contestant/files/var/lib/cloud/scripts/per-instance/generate-env_aws.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +cat << _EOF_ > /home/isucon/env.sh +MYSQL_HOST="127.0.0.1" +MYSQL_PORT=3306 +MYSQL_USER=isucon +MYSQL_DBNAME=isucondition +MYSQL_PASS=isucon +ISU_CONDITION_IP="$(curl -s http://169.254.169.254/latest/meta-data/public-ipv4)" +_EOF_ +chown isucon: /home/isucon/env.sh diff --git a/provisioning/ansible/roles/contestant/tasks/generate_env.yml b/provisioning/ansible/roles/contestant/tasks/generate_env.yml new file mode 100644 index 000000000..9fb496837 --- /dev/null +++ b/provisioning/ansible/roles/contestant/tasks/generate_env.yml @@ -0,0 +1,9 @@ +--- +- name: "roles/contestant/tasks/generate_env: Deploy generate-env.sh" + tags: + - aws + become_user: root + copy: + src: "var/lib/cloud/scripts/per-instance/generate-env_aws.sh" + dest: "/var/lib/cloud/scripts/per-instance/generate-env.sh" + mode: "0755" diff --git a/provisioning/ansible/roles/contestant/tasks/isucondition.yml b/provisioning/ansible/roles/contestant/tasks/isucondition.yml index 8dab7c022..99fa84279 100644 --- a/provisioning/ansible/roles/contestant/tasks/isucondition.yml +++ b/provisioning/ansible/roles/contestant/tasks/isucondition.yml @@ -8,14 +8,8 @@ owner: isucon group: isucon -- name: "roles/contestant/tasks/isucondition: Deploy env.sh" - become_user: isucon - copy: - src: "home/isucon/env.sh" - dest: "/home/isucon/env.sh" - owner: "isucon" - group: "isucon" - mode: "0644" +- name: "roles/contestant/tasks/isucondition: Include generate_env.yml" + include: generate_env.yml - name: "roles/contestant/tasks/isucondition: Include isucondition-frontend.yml" include: isucondition-frontend.yml @@ -30,7 +24,6 @@ systemd: daemon_reload: "yes" name: "isucondition.go.service" - state: "restarted" enabled: "yes" - name: "roles.contestant.tasks.isucondition: Deploy isucon11 initial-data" diff --git a/provisioning/packer/base.libsonnet b/provisioning/packer/base.libsonnet index a9ea24238..93aa6ead5 100644 --- a/provisioning/packer/base.libsonnet +++ b/provisioning/packer/base.libsonnet @@ -10,6 +10,7 @@ aws_access_key: "{{env `AWS_ACCESS_KEY_ID`}}", aws_secret_key: "{{env `AWS_SECRET_ACCESS_KEY`}}", aws_session_token: "{{env `AWS_SESSION_TOKEN`}}", + git_tag: "{{env `GIT_TAG`}}", }, builder_ec2:: { @@ -35,6 +36,7 @@ Packer: '1', Family: 'isucon11q-' + $.arg_arch + '-' + $.arg_variant, Project: 'qualify-dev', + GitTag: '{{user "git_tag"}}' }, // TODO: spot instance を利用する @@ -55,16 +57,6 @@ ssh_interface: 'public_ip', associate_public_ip_address: true, - //vpc_id: 'vpc-0ee05560be5a92944', - // subnet_filter: { - // filters: { - // 'vpc-id': 'vpc-0ee05560be5a92944', - // 'tag:Tier': 'public', - // 'availability-zone': 'ap-northeast-1c', - // }, - // random: true, - // }, - run_tags: { Name: 'packer-isucon11q-' + $.arg_arch + '-' + $.arg_variant, Project: 'qualify-dev', @@ -156,7 +148,7 @@ run_ansible: { type: 'shell', inline: [ - '( cd /dev/shm/ansible && sudo ansible-playbook -u root -i hosts -t ' + $.arg_variant + ' site.yml )', + '( cd /dev/shm/ansible && sudo ansible-playbook -u root -i hosts -t aws -t ' + $.arg_variant + ' site.yml )', ], }, remove_ansible: { diff --git a/webapp/frontend/src/components/Condition/ConditionDetail.tsx b/webapp/frontend/src/components/Condition/ConditionDetail.tsx index 91f27790b..fd962a982 100644 --- a/webapp/frontend/src/components/Condition/ConditionDetail.tsx +++ b/webapp/frontend/src/components/Condition/ConditionDetail.tsx @@ -6,7 +6,7 @@ interface Props { } const getTime = (condition: Condition) => { - const date = new Date(condition.timestamp) + const date = new Date(condition.timestamp * 1000) // 2020/01/01 01:01:01 return `${date.getUTCFullYear()}/${pad0(date.getUTCMonth() + 1)}/${pad0( date.getUTCDate() diff --git a/webapp/frontend/src/components/IsuDetail/NameEdit.tsx b/webapp/frontend/src/components/IsuDetail/NameEdit.tsx index ac69f6e53..edd144f81 100644 --- a/webapp/frontend/src/components/IsuDetail/NameEdit.tsx +++ b/webapp/frontend/src/components/IsuDetail/NameEdit.tsx @@ -48,10 +48,16 @@ const FinishEditButtons = ({ }) => { return (
- -
@@ -60,7 +66,10 @@ const FinishEditButtons = ({ const StartEditButton = ({ startEdit }: { startEdit: () => void }) => { return ( - ) diff --git a/webapp/frontend/src/components/IsuGraph/DateInput.tsx b/webapp/frontend/src/components/IsuGraph/DateInput.tsx index 4cfddf79d..af7093b5c 100644 --- a/webapp/frontend/src/components/IsuGraph/DateInput.tsx +++ b/webapp/frontend/src/components/IsuGraph/DateInput.tsx @@ -28,11 +28,4 @@ const DateInput = ({ day, fetchGraphs }: Props) => { ) } -const dateToStr = (date: Date) => { - return `${date.getUTCFullYear()}/${pad0(date.getUTCMonth() + 1)}/${pad0( - date.getUTCDate() - )} ` -} -const pad0 = (num: number) => ('0' + num).slice(-2) - export default DateInput diff --git a/webapp/frontend/src/components/PageHeader/UserControlModal.tsx b/webapp/frontend/src/components/PageHeader/UserControlModal.tsx index 9e8fff77b..e64aa8105 100644 --- a/webapp/frontend/src/components/PageHeader/UserControlModal.tsx +++ b/webapp/frontend/src/components/PageHeader/UserControlModal.tsx @@ -2,6 +2,7 @@ import Modal from 'react-modal' import { IoMdLogOut } from 'react-icons/io' import { Link } from 'react-router-dom' import { useDispatchContext } from '../../context/state' +import apis from '../../lib/apis' interface Props { isOpen: boolean @@ -10,7 +11,8 @@ interface Props { const UserControlModal = (props: Props) => { const dispatch = useDispatchContext() - const logout = () => { + const logout = async () => { + await apis.postSignout() dispatch({ type: 'logout' }) }