Repository: isucon/isucon11-qualify (language: go)

Issue:
[bench] 存在していないISUのconditionを投げるようにする
## WHY
* backendの post isuconditionエンドポイントで、ISUの存在チェックを消した方が得点があがってしまう
  - bench側でレスポンスのチェックをしていないので

## WHAT
* ベンチ側から存在していないISUのconditionを投げるようにする
  - ISUの存在チェックを消すと、他のGETのエンドポイントでエラーが起きるようになる
*  存在チェックを消しても得点があがりそうなバランスはありそうだが、仕方がない

## 備考
* 議論したslackのスレッド
  -  https://isucon.slack.com/archives/C021YD1HXUL/p1627948777152500


Discussion / hints:
> 他のGETのエンドポイントでエラーが起きるようになる

存在してないISUに対するGETリクエストは送れないのでエラーは起きないです
ただ無駄にInsertさせるというだけです

よくよく考えるとメインのPOST condition でのinsertが大量すぎて弾くと良くなるほどお邪魔のリクエストを送るのは難しそう
まだ存在しないISUのconditionをPOSTしまくって、そのあとにそのISUをつくってconditionのGETをして空が返ってこないなら落とす(fail)
これで検証できる
prepare でやる
正常系
1. 存在しないISU UUid = (A)に対するpost condition：404 x N
2. UUIDをAとして、ISUを作る
3. GET /api/condition/Aは空が返ってくる

異常系
1. 存在しないISU UUid = (A)に対するpost condition：200 x N
2. UUIDをAとして、ISUを作る
3. GET /api/condition/Aは1でpost したデータが返ってくる
eiyaさんに確認した内容
* Prepareの最初の方でtimeout50msで1秒間post isuconditionをgoroutineで回すようにしておいて、prepareの最後のほうでこの検証する形で良さそう
* backend実装でpost isuconditionをランダムに落としてるので100%確実な検証はできない前提のチェック
* 優先度はlabelについてる通りできたらやる程度なので低め

Fix patch:
diff --git a/bench/scenario/jiaapi.go b/bench/scenario/jiaapi.go
index 2c30469ae..4f07e1ba6 100644
--- a/bench/scenario/jiaapi.go
+++ b/bench/scenario/jiaapi.go
@@ -89,49 +89,50 @@ func (s *Scenario) postActivate(c echo.Context) error {
 	state := &service.JIAServiceRequest{}
 	err := c.Bind(state)
 	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest)
+		return c.String(http.StatusBadRequest, "Bad Request")
 	}
 	targetBaseURL, err := url.Parse(state.TargetBaseURL)
 	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest)
+		return c.String(http.StatusBadRequest, "Bad URL")
 	}
+	//TODO: URLの検証
 
 	//poster Goroutineの起動
 	var isu *model.Isu
 	var scenarioChan *model.StreamsForPoster
 	var fqdn string
 	posterContext := posterRootContext
-	err = func() error {
+	errCode, errMsg := func() (int, string) {
 		var ok bool
 		streamsForPosterMutex.Lock()
 		defer streamsForPosterMutex.Unlock()
 		// scenario goroutine とやり取りするためのチャネルを受け取る
 		scenarioChan, ok = streamsForPoster[state.IsuUUID]
 		if !ok {
-			return echo.NewHTTPError(http.StatusNotFound)
+			return http.StatusNotFound, "Bad isu_uuid"
 		}
 		// リクエストされた JIA_ISU_UUID が事前に scenario.NewIsu にて作成された isu と紐付かない場合 404 を返す
 		isu, ok = isuFromUUID[state.IsuUUID]
 		if !ok {
 			//scenarioChanでチェックしているのでここには来ないはず
-			return echo.NewHTTPError(http.StatusNotFound)
+			return http.StatusNotFound, "Bad isu_uuid"
 		}
 		_, ok = isuIsActivated[state.IsuUUID]
 		if ok {
 			//activate済み
-			return nil
+			return 0, ""
 		}
 
 		// useTLS が有効 && POST isucondition する URL に https 以外が指定されていたら 400 を返す
 		if s.UseTLS && targetBaseURL.Scheme != "https" {
-			return echo.NewHTTPError(http.StatusBadRequest)
+			return http.StatusBadRequest, "Bad URL Scheme"
 		}
 		// FQDN が競技者 VM のものでない場合 400 を返す
 		fqdn = targetBaseURL.Hostname()
 		port := targetBaseURL.Port()
 		ipAddr, ok := s.GetIPAddrFromFqdn(fqdn)
 		if !ok {
-			return echo.NewHTTPError(http.StatusBadRequest)
+			return http.StatusBadRequest, "Bad URL: hostname must be isucondition[1-3].t.isucon.dev"
 		}
 		// URL の文字列を IP アドレスに変換
 		if port != "" {
@@ -148,10 +149,10 @@ func (s *Scenario) postActivate(c echo.Context) error {
 			defer s.loadWaitGroup.Done()
 			s.keepPosting(posterContext, targetBaseURL, fqdn, isu, scenarioChan)
 		}()
-		return nil
+		return 0, ""
 	}()
-	if err != nil {
-		return err
+	if errCode != 0 {
+		return c.String(errCode, errMsg)
 	}
 
 	time.Sleep(50 * time.Millisecond)
diff --git a/bench/scenario/prepare.go b/bench/scenario/prepare.go
index 1fb115269..47cc10bfc 100644
--- a/bench/scenario/prepare.go
+++ b/bench/scenario/prepare.go
@@ -106,11 +106,8 @@ func (s *Scenario) prepareCheck(parent context.Context, step *isucandar.Benchmar
 	ctx, cancel := context.WithCancel(parent)
 	defer cancel()
 
-	//ユーザー作成
-	guestAgent, err := s.NewAgent(agent.WithTimeout(s.prepareTimeout))
-	if err != nil {
-		logger.AdminLogger.Panicln(err)
-	}
+	//存在しないISUのPOST
+	unregisteredIsu, postCancel, postWait := s.prepareStartInvalidIsuPost(ctx)
 
 	// 正常系Prepare Check
 	s.prepareNormal(ctx, step)
@@ -118,6 +115,11 @@ func (s *Scenario) prepareCheck(parent context.Context, step *isucandar.Benchmar
 		return failure.NewError(ErrCritical, fmt.Errorf("アプリケーション互換性チェックに失敗しました"))
 	}
 
+	//ユーザー作成
+	guestAgent, err := s.NewAgent(agent.WithTimeout(s.prepareTimeout))
+	if err != nil {
+		logger.AdminLogger.Panicln(err)
+	}
 	noIsuAgent, err := s.NewAgent(agent.WithTimeout(s.prepareTimeout))
 	if err != nil {
 		logger.AdminLogger.Panicln(err)
@@ -145,12 +147,18 @@ func (s *Scenario) prepareCheck(parent context.Context, step *isucandar.Benchmar
 	s.prepareIrregularCheckGetIsu(ctx, isuconUser, noIsuUser, guestAgent, step)
 	s.prepareIrregularCheckGetIsuIcon(ctx, isuconUser, noIsuUser, guestAgent, step)
 	s.prepareIrregularCheckGetIsuGraph(ctx, isuconUser, noIsuUser, guestAgent, step)
-	s.prepareIrregularCheckGetIsuConditions(ctx, isuconUser, noIsuUser, guestAgent, step)
+	s.prepareIrregularCheckGetIsuConditions(ctx, isuconUser, noIsuUser, guestAgent, step, unregisteredIsu)
 
 	// MEMO: postIsuConditionのprepareチェックは確率で失敗して安定しないため、prepareステップでは行わない
 
+	//post終了
+	postCancel()
+	<-postWait
+	unregisteredIsu.Conditions = model.NewIsuConditionArray()
+
 	// ユーザのISUが増えるので他の検証終わった後に実行
 	s.prepareCheckPostIsu(ctx, isuconUser, noIsuUser, guestAgent, step)
+	s.prepareCheckPostIsuWithPrevCondition(ctx, noIsuUser, step, unregisteredIsu)
 	if hasErrors() {
 		return failure.NewError(ErrCritical, fmt.Errorf("アプリケーション互換性チェックに失敗しました"))
 	}
@@ -931,7 +939,7 @@ func (s *Scenario) prepareIrregularCheckGetIsuGraph(ctx context.Context, loginUs
 	}
 }
 
-func (s *Scenario) prepareIrregularCheckGetIsuConditions(ctx context.Context, loginUser *model.User, noIsuUser *model.User, guestAgent *agent.Agent, step *isucandar.BenchmarkStep) {
+func (s *Scenario) prepareIrregularCheckGetIsuConditions(ctx context.Context, loginUser *model.User, noIsuUser *model.User, guestAgent *agent.Agent, step *isucandar.BenchmarkStep, unregisteredIsu *model.Isu) {
 	isu := loginUser.IsuListOrderByCreatedAt[0]
 
 	// condition の read lock を取得
@@ -1050,7 +1058,7 @@ func (s *Scenario) prepareIrregularCheckGetIsuConditions(ctx context.Context, lo
 	query = url.Values{}
 	query.Set("end_time", strconv.FormatInt(lastTime, 10))
 	query.Set("condition_level", "info,warning,critical")
-	resBody, res, err = getIsuConditionErrorAction(ctx, loginUser.Agent, "jiaisuuuid", query)
+	resBody, res, err = getIsuConditionErrorAction(ctx, loginUser.Agent, unregisteredIsu.JIAIsuUUID, query)
 	if err != nil {
 		step.AddError(err)
 		return
@@ -1064,3 +1072,101 @@ func (s *Scenario) prepareIrregularCheckGetIsuConditions(ctx context.Context, lo
 		return
 	}
 }
+
+func (s *Scenario) prepareStartInvalidIsuPost(ctx context.Context) (*model.Isu, context.CancelFunc, <-chan struct{}) {
+	isu, streamsForPoster, err := model.NewRandomIsuRaw(nil)
+	if err != nil {
+		logger.AdminLogger.Panic(err)
+	}
+
+	//ISU協会にIsu*を登録する必要あり
+	RegisterToJiaAPI(isu, streamsForPoster)
+
+	targetBaseURL, err := url.Parse(s.BaseURL)
+	if err != nil {
+		logger.AdminLogger.Panic(err)
+	}
+
+	posterStop := make(chan struct{})
+	posterCtx, cancel := context.WithCancel(ctx)
+	go func() {
+		defer close(posterStop)
+		s.keepPosting(posterCtx, targetBaseURL, agent.DefaultTLSConfig.ServerName, isu, streamsForPoster)
+	}()
+
+	return isu, cancel, posterStop
+}
+
+func (s *Scenario) prepareCheckPostIsuWithPrevCondition(ctx context.Context, loginUser *model.User, step *isucandar.BenchmarkStep, baseIsu *model.Isu) {
+	//Isuの登録 e.POST("/api/isu", postIsu)
+	// check: 事前にconditionがPOSTされた椅子の登録（正常に弾かれているかをチェックしたい）
+	if err := BrowserAccess(ctx, loginUser.Agent, "/register", RegisterPage); err != nil {
+		step.AddError(err)
+		return
+	}
+
+	postTime := s.ToVirtualTime(time.Now())
+
+	//POST
+	baseIsu.Owner = loginUser
+	image, err := random.Image()
+	if err != nil {
+		logger.AdminLogger.Panic(err)
+	}
+	baseIsu.SetImage(image)
+	postResp, res, err := postIsuAction(ctx, loginUser.Agent, service.PostIsuRequest{
+		JIAIsuUUID: baseIsu.JIAIsuUUID,
+		IsuName:    baseIsu.Name,
+		Img:        image,
+	})
+	if err != nil {
+		addErrorWithContext(ctx, step, err)
+		return
+	}
+	baseIsu.ID = postResp.ID
+	err = verifyIsu(res, baseIsu, postResp)
+	if err != nil {
+		addErrorWithContext(ctx, step, err)
+		return
+	}
+
+	//ISU詳細にリダイレクトされる
+	isuResponse, res, err := getIsuIdAction(ctx, loginUser.Agent, baseIsu.JIAIsuUUID)
+	if err != nil {
+		addErrorWithContext(ctx, step, err)
+		return
+	}
+	err = verifyIsu(res, baseIsu, isuResponse)
+	if err != nil {
+		addErrorWithContext(ctx, step, err)
+		return
+	}
+	imageRes, res, err := getIsuIconAction(ctx, loginUser.Agent, baseIsu.JIAIsuUUID)
+	if err != nil {
+		addErrorWithContext(ctx, step, err)
+		return
+	}
+	if baseIsu.ImageHash != md5.Sum(imageRes) {
+		step.AddError(errorInvalid(res, "期待するISUアイコンと一致しません"))
+		return
+	}
+	loginUser.AddIsu(baseIsu)
+
+	//GET condition
+	req := service.GetIsuConditionRequest{
+		StartTime:      nil,
+		EndTime:        postTime.Unix(),
+		ConditionLevel: "info,warning,critical",
+	}
+	conditionsTmp, res, err := getIsuConditionAction(ctx, loginUser.Agent, baseIsu.JIAIsuUUID, req)
+	if err != nil {
+		step.AddError(err)
+		return
+	}
+	//検証
+	err = verifyPrepareIsuConditions(res, loginUser, baseIsu.JIAIsuUUID, &req, conditionsTmp)
+	if err != nil {
+		step.AddError(err)
+		return
+	}
+}
diff --git a/bench/scenario/verify.go b/bench/scenario/verify.go
index 2c6403356..fe2664948 100644
--- a/bench/scenario/verify.go
+++ b/bench/scenario/verify.go
@@ -104,6 +104,19 @@ func verifyIsuNotFound(res *http.Response, text string) error {
 
 //データ整合性チェック
 
+//Icon,LatestIsuConditionを除いたISUの整合性チェック
+func verifyIsu(res *http.Response, expected *model.Isu, actual *service.Isu) error {
+	if actual.JIAIsuUUID != expected.JIAIsuUUID {
+		return errorMismatch(res, "椅子が異なります(expected %s, actual %s)", expected.JIAIsuUUID, actual.JIAIsuUUID)
+	}
+	if actual.ID != expected.ID ||
+		actual.Character != expected.Character ||
+		actual.Name != expected.Name {
+		return errorMismatch(res, "椅子(JIA_ISU_UUID=%s)の情報が異なります", expected.JIAIsuUUID)
+	}
+	return nil
+}
+
 func verifyIsuList(res *http.Response, expectedReverse []*model.Isu, isuList []*service.Isu) ([]string, []error) {
 	errs := []error{}
 	var newConditionUUIDs []string


Test patch: