Y

ISUCON12予選の復習をしました 7

この記事は何か?

ISUCON12予選の復習記録です。

第7回はISUCON12 予選の解説 (Node.jsでSQLiteのまま10万点行く方法)の「7 Finish APIでBillingReportを生成する」を試します。

目次

1.「7 Finish APIでBillingReportを生成する」を試す

1.「7 Finish APIでBillingReportを生成する」を試す

今回の対応は難しいです。これができた人は尊敬します。

反映までの猶予時間について
一部APIは他のリクエストへの反映までに許容される猶予時間があります

POST /api/organizer/competition/:competition_id/finish
以下のエンドポイントは上記APIの情報の反映まで、レスポンスを返してから3秒の猶予が許容されます

GET /api/organizer/billing
GET /api/admin/tenants/billing

上記は「ISUCON12 予選当日マニュアル」に記載されています。ヒントですがこれが何を意味しているか分かりませんでした。過去問でも同じような「許容される猶予時間」についての記載があるので、慣れる必要があります。(ISUCON文学に慣れる必要がある)

この意味は「/finishエンドポイントの完了後、3秒以内には、結果反映を検証するベンチ処理は発生しない」という意味だと今は理解しています。正解は今後ベンチマークデバッグする時に答え合わせします。

エンドポイント「テナントごとの課金レポート /api/admin/tenants/billing」は、テナント毎の課金レポートを出力しますが、全テナントの全大会のデータを集計する必要があり、大変重い処理です。「この集計処理を/billingではなく、/finishエンドポイントで事前に実行しておく」というのが今回の対応になります。

この発想に気づくことも僕には難しいですが、実装するのも注意が必要です。以下プロセスで行います。

1. テーブル作成
2. /initializeエンドポイントで初期データ分の集計処理を追加
3. /finishエンドポイントにデータ登録処理を追加
4. /billingエンドポイントにデータ参照処理を追加
5. /initializeエンドポイントで実行されるデータ初期化処理に追加 (TRUNCATE追加)
  • テーブル作成
CREATE TABLE `billing_report` (
  `tenant_id` BIGINT UNSIGNED NOT NULL,
  `competition_id` VARCHAR(255) NOT NULL,
  `competition_title` VARCHAR(255) NOT NULL,
  `player_count` BIGINT NOT NULL,
  `visitor_count` BIGINT NOT NULL,
  `billing_player_yen` BIGINT NOT NULL,
  `billing_visitor_yen` BIGINT NOT NULL,
  `billing_yen` BIGINT NOT NULL,
  PRIMARY KEY(`tenant_id`, `competition_id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;
diff --git a/go/isuports.go b/go/isuports.go
index dc182fb..c943073 100644
--- a/go/isuports.go
+++ b/go/isuports.go
@@ -449,6 +449,17 @@ type PlayerScoreJoinPlayerRow struct {
        DisplayName   string `db:"display_name"`
 }

+type BillingReportRow struct {
+       TenantID          int64  `db:"tenant_id"`
+       CompetitionID     string `db:"competition_id"`
+       CompetitionTitle  string `db:"competition_title"`
+       PlayerCount       int64  `db:"player_count"`
+       VisitorCount      int64  `db:"visitor_count"`
+       BillingPlayerYen  int64  `db:"billing_player_yen"`
+       BillingVisitorYen int64  `db:"billing_visitor_yen"`
+       BillingYen        int64  `db:"billing_yen"`
+}
+
 // 排他ロックのためのファイル名を生成する
 func lockFilePath(id int64) string {
        tenantDBDir := getEnv("ISUCON_TENANT_DB_DIR", "../tenant_db")
@@ -717,11 +728,32 @@ func tenantsBillingHandler(c echo.Context) error {
                                return fmt.Errorf("failed to Select competition: %w", err)
                        }
                        for _, comp := range cs {
+                               /*
                                report, err := billingReportByCompetition(ctx, tenantDB, t.ID, comp.ID)
                                if err != nil {
                                        return fmt.Errorf("failed to billingReportByCompetition: %w", err)
                                }
                                tb.BillingYen += report.BillingYen
+                               */
+
+                               // 請求情報テーブルから取得
+                               br := BillingReportRow{}
+                               if err := adminDB.GetContext(
+                                       ctx,
+                                       &br,
+                                       // 最後にCSVに登場したスコアを採用する = row_numが一番大きいもの
+                                       "SELECT * FROM billing_report WHERE tenant_id = ? AND competition_id = ?",
+                                       t.ID,
+                                       comp.ID,
+                               ); err != nil {
+                                       // 行がない = スコアが記録されてない
+                                       if errors.Is(err, sql.ErrNoRows) {
+                                               continue
+                                       }
+                                       return fmt.Errorf("failed to billingReportByCompetition: %w", err)
+                               }
+
+                               tb.BillingYen += br.BillingYen
                        }
                        tenantBillings = append(tenantBillings, tb)
                        return nil
@@ -1008,6 +1040,13 @@ func competitionFinishHandler(c echo.Context) error {
                        now, now, id, err,
                )
        }
+
+       // 請求情報保存 TODO 非同期処理
+       err = insertBillingReport(ctx, tenantDB, v.tenantID, id)
+       if err != nil {
+               return fmt.Errorf("error insertBillingReport: %w", err)
+       }
+
        return c.JSON(http.StatusOK, SuccessResult{Status: true})
 }

@@ -1690,5 +1729,59 @@ func initializeHandler(c echo.Context) error {
        res := InitializeHandlerResult{
                Lang: "go",
        }
+
+       // 請求情報保存
+       ctx := context.Background()
+
+       ts := []TenantRow{}
+       if err := adminDB.SelectContext(ctx, &ts, "SELECT * FROM tenant ORDER BY id DESC"); err != nil {
+               return fmt.Errorf("error Select tenant: %w", err)
+       }
+
+       for _, t := range ts {
+               tenantDB, err := connectToTenantDB(t.ID)
+               if err != nil {
+                       return fmt.Errorf("failed to connectToTenantDB: %w", err)
+               }
+               defer tenantDB.Close()
+               cs := []CompetitionRow{}
+               if err := tenantDB.SelectContext(
+                       ctx,
+                       &cs,
+                       "SELECT * FROM competition ORDER BY created_at DESC",
+               ); err != nil {
+                       return fmt.Errorf("error Select competition: %w", err)
+               }
+
+               for _, comp := range cs {
+                       err := insertBillingReport(ctx, tenantDB, comp.TenantID, comp.ID)
+                       if err != nil {
+                               return fmt.Errorf("error insertBillingReport: %w", err)
+                       }
+               }
+       }
+
        return c.JSON(http.StatusOK, SuccessResult{Status: true, Data: res})
 }
+
+// 請求情報保存
+func insertBillingReport(ctx context.Context, tenantDB dbOrTx, tenantID int64, competitonID string) error {
+       report, err := billingReportByCompetition(ctx, tenantDB, tenantID, competitonID)
+       if err != nil {
+               return fmt.Errorf("failed to billingReportByCompetition: %w", err)
+       }
+
+       if _, err := adminDB.ExecContext(
+               ctx,
+               "INSERT INTO billing_report (tenant_id, competition_id, competition_title, player_count, visitor_count, billing_player_yen, billing_visitor_yen, billing_yen) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+               tenantID, report.CompetitionID, report.CompetitionTitle, report.PlayerCount, report.VisitorCount, report.BillingPlayerYen, report.BillingVisitorYen, report.BillingYen,
+       ); err != nil {
+               return fmt.Errorf(
+                       "error Insert billing_report: %w",
+                       err,
+               )
+       }
+
+       return nil
+}
+
diff --git a/sql/init.sql b/sql/init.sql
index 990be9a..b4ff9e2 100644
--- a/sql/init.sql
+++ b/sql/init.sql
@@ -2,3 +2,4 @@ DELETE FROM tenant WHERE id > 100;
 DELETE FROM visit_history WHERE created_at >= '1654041600';
 UPDATE id_generator SET id=2678400000 WHERE stub='a';
 ALTER TABLE id_generator AUTO_INCREMENT=2678400000;
+TRUNCATE table billing_report;
01:57:29.017960 SCORE: 5617 (+5617 0(0%))
09:21:44.540379 SCORE: 9868 (+11747 -1879(16%))

感想

スコアが大幅に増えましたが、失敗によるマイナスが発生しています。上記のコード修正では足りない何かがあるからだと思います。詳細は今後別の人の解説を見て解決します。

また、「請求情報保存 TODO 非同期処理」の部分は解説にあるとおり今後goroutineを利用してみます。

予選当日マニュアルに書かれている「許容される猶予時間」の対応方法については慣れておきたいです。また、今回テーブルを増やしましたが、テーブル増やすことも毎回あるのでそういう発想を忘れないようにしたいです。

今回SQLiteへの検索の一部をMySQLに移しましたが、これは3台構成に変更するための布石となっています。このあたりは次回以降記載します。

参考

1 ISUCON12 予選の解説 (Node.jsでSQLiteのまま10万点行く方法)

2 ISUCON12 予選当日マニュアル