Open Matchのデータの流れを追う

[グレンジ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/grenge) 20日目担当の中陳です。

弊社ではゲームマッチングシステムとして[Open Match](https://github.com/googleforgames/open-match)を導入しています。
Open MatchはGoogleが開発しているオープンソースのゲームマッチメイキングフレームワークです。

Open Matchのアーキテクチャの説明については、公式ドキュメントを参照してください。
https://open-match.dev/site/docs/guides/matchmaker/

今年の「Google Cloud INSIDE Games & Apps」で発表した資料もよければご覧ください。
https://storage.googleapis.com/gweb-cloudnext19.appspot.com/event-assets/tokyo/materials/D1-5-I08.pdf

「[Google Cloud Japan Customer Engineer Advent Calendar 2019](https://medium.com/google-cloud-jp/gcp-ce-advent-calendar-2019-1aeb8ebda7b1)」でも、12/22に「[Open Match 超入門](https://medium.com/google-cloud-jp/open-match-super101-aaf3e54bd4e7)」が予定されているようなので、興味がある人はチェックしてみてください。

Open Matchの理解を深めるために、デモを動かしそのデータの流れを追ってみたいと思います。
データのやり取りはRedisを介して行われています。

## 準備

下記ドキュメントを参考に Core Open Match と Demo をインストールします。

- [Install Core Open Match](https://open-match.dev/site/docs/installation/yaml/#install-core-open-match)
- [Getting Started Guide](https://open-match.dev/site/docs/getting-started/)

```
# Install the core Open Match services.
kubectl apply --namespace open-match \
  -f https://open-match.dev/install/v0.8.0/yaml/01-open-match-core.yaml
```

```
kubectl apply --namespace open-match \
  -f https://open-match.dev/install/v0.8.0/yaml/06-open-match-override-configmap.yaml \
  -f https://open-match.dev/install/v0.8.0/yaml/07-open-match-default-evaluator.yaml
```

```
kubectl create namespace open-match-demo
kubectl apply --namespace open-match-demo \
  -f https://open-match.dev/install/v0.8.0/yaml/02-open-match-demo.yaml
```

```
kubectl port-forward --namespace open-match-demo service/om-demo 51507:51507
```

http://localhost:51507/ にアクセスすると、次のように表示されます。

```
{
  "clients": {
    "fakeplayer_0": {
      "Status": "Sleeping (pretend this is playing a match...)",
      "Assignment": {
        "connection": "85.98.86.16:2222"
      }
    },
    "fakeplayer_1": {
      "Status": "Main Menu",
      "Assignment": null
    },
    "fakeplayer_2": {
      "Status": "Waiting match with ticket Id bnrnjbh4tpl76e14h460",
      "Assignment": null
    },
    "fakeplayer_3": {
      "Status": "Sleeping (pretend this is playing a match...)",
      "Assignment": {
        "connection": "85.98.86.16:2222"
      }
    },
    "fakeplayer_4": {
      "Status": "Waiting match with ticket Id bnrnjf14tpl76e14h470",
      "Assignment": null
    }
  },
  "director": {
    "Status": "Sleeping",
    "LatestMatches": [
      {
        "match_id": "profile-1v1-time-2019-12-16T12:33:59.73-num-0",
        "match_profile": "1v1",
        "match_function": "a-simple-1v1-matchfunction",
        "tickets": [
          {
            "id": "bnrnjbp4tpl76e14h46g"
          },
          {
            "id": "bnrnjb14tpl76e14h45g"
          }
        ]
      }
    ]
  },
  "uptime": 1374
}
```

プレイヤ (Client) が Open Match (Frontend) に対してリクエストを送り、マッチが完了するのを待ちます。
Director は Open Match (Backend) マッチ実行をリクエストし、完了したマッチに対してゲームサーバーの割り当てを行います。

下記デモのソースコードをベースに追っていきます。

- [clients.go](https://github.com/googleforgames/open-match/blob/release-0.8/examples/demo/components/clients/clients.go)
- [director.go](https://github.com/googleforgames/open-match/blob/release-0.8/examples/demo/components/director/director.go)

Redisのデータの流れの確認には、MONITOR コマンドを使います。

```
127.0.0.1:6379> MONITOR
```

## Client → Open Match (Frontend)

### チケット作成

CreateTicketでチケット作成を行います。

```go:clients.go
	s.Status = "Creating Open Match Ticket"
	update(s)

	var ticketId string
	{
		req := &pb.CreateTicketRequest{
			Ticket: &pb.Ticket{},
		}

		resp, err := fe.CreateTicket(ctx, req)
		if err != nil {
			panic(err)
		}
		ticketId = resp.Ticket.Id
	}
```

```text:1プレイヤ目
1576500156.947800 [0 10.1.13.146:55264] "MULTI"
1576500156.947826 [0 10.1.13.146:55264] "SET" "bnrnnf14tpl76e14h5a0" "\n\x14bnrnnf14tpl76e14h5a0"
1576500156.947901 [0 10.1.13.146:55264] "EXPIRE" "bnrnnf14tpl76e14h5a0" "43200"
1576500156.947906 [0 10.1.13.146:55264] "EXEC"
1576500156.948580 [0 10.1.13.146:55264] "MULTI"
1576500156.948589 [0 10.1.13.146:55264] "SADD" "ic$bnrnnf14tpl76e14h5a0" "allTickets"
1576500156.948737 [0 10.1.13.146:55264] "ZADD" "allTickets" "0" "bnrnnf14tpl76e14h5a0"
1576500156.948750 [0 10.1.13.146:55264] "EXEC"
```

```text:2プレイヤ目
1576500159.893873 [0 10.1.13.146:59696] "MULTI"
1576500159.893893 [0 10.1.13.146:59696] "SET" "bnrnnfp4tpl76e14h5b0" "\n\x14bnrnnfp4tpl76e14h5b0"
1576500159.893903 [0 10.1.13.146:59696] "EXPIRE" "bnrnnfp4tpl76e14h5b0" "43200"
1576500159.893909 [0 10.1.13.146:59696] "EXEC"
1576500159.894889 [0 10.1.13.146:59696] "MULTI"
1576500159.894901 [0 10.1.13.146:59696] "SADD" "ic$bnrnnfp4tpl76e14h5b0" "allTickets"
1576500159.895011 [0 10.1.13.146:59696] "ZADD" "allTickets" "0" "bnrnnfp4tpl76e14h5b0"
1576500159.895022 [0 10.1.13.146:59696] "EXEC"
```

以下のデータが書き込まれています。

- チケットIDのキーにチケット情報
- チケットIDに紐付くインデックスキー
- インデックスキー（ここではallTickets）にチケットIDと検索値

### マッチ完了待ち

GetAssignmentsでマッチ結果を待ちます。
結果のConnection文字列が設定されいる場合にマッチ完了となります。

```go:clients.go
	s.Status = fmt.Sprintf("Waiting match with ticket Id %s", ticketId)
	update(s)

	var assignment *pb.Assignment
	{
		req := &pb.GetAssignmentsRequest{
			TicketId: ticketId,
		}

		stream, err := fe.GetAssignments(ctx, req)
		for assignment.GetConnection() == "" {
			resp, err := stream.Recv()
			if err != nil {
				// For now we don't expect to get EOF, so that's still an error worthy of panic.
				panic(err)
			}

			assignment = resp.Assignment
		}

		err = stream.CloseSend()
		if err != nil {
			panic(err)
		}
	}
```

```text:1プレイヤ目
1576500156.951293 [0 10.1.13.146:47834] "GET" "bnrnnf14tpl76e14h5a0"
1576500157.051971 [0 10.1.13.146:47834] "GET" "bnrnnf14tpl76e14h5a0"
1576500157.153294 [0 10.1.13.146:47834] "GET" "bnrnnf14tpl76e14h5a0"
1576500157.254225 [0 10.1.13.146:47834] "GET" "bnrnnf14tpl76e14h5a0"
```

```text:2プレイヤ目
1576500159.897335 [0 10.1.13.146:34212] "GET" "bnrnnfp4tpl76e14h5b0"
1576500159.998534 [0 10.1.13.146:34212] "GET" "bnrnnfp4tpl76e14h5b0"
1576500160.100184 [0 10.1.13.146:34212] "GET" "bnrnnfp4tpl76e14h5b0"
1576500160.200872 [0 10.1.13.146:34212] "GET" "bnrnnfp4tpl76e14h5b0"
```

チケットIDキーがループで参照されています。
チケットのAssignment情報に変更があった場合に、クライアントに返されます。

## Director → Open Match (Backend)

### マッチ実行

FetchMatchesを使って、マッチ実行及び、マッチ結果の受け取りを行います。
このデモでは、1v1の条件なしのマッチ実行を行っています。

```go:director.go
	s.Status = "Match Match: Sending Request"
	ds.Update(s)

	var matches []*pb.Match
	{
		req := &pb.FetchMatchesRequest{
			Config: &pb.FunctionConfig{
				Host: "om-function.open-match-demo.svc.cluster.local",
				Port: 50502,
				Type: pb.FunctionConfig_GRPC,
			},
			Profiles: []*pb.MatchProfile{
				{
					Name: "1v1",
					Pools: []*pb.Pool{
						{
							Name: "Everyone",
						},
					},
				},
			},
		}

		stream, err := be.FetchMatches(ds.Ctx, req)
		if err != nil {
			panic(err)
		}

		for {
			resp, err := stream.Recv()
			if err == io.EOF {
				break
			}
			if err != nil {
				panic(err)
			}
			matches = append(matches, resp.GetMatch())
		}
	}
```

```text:プールからチケット取得
1576500160.702074 [0 10.1.13.148:52540] "ZRANGEBYSCORE" "proposed_ticket_ids" "1576500100701965766" "1576500160701965766"
1576500160.702482 [0 10.1.13.148:52540] "ZRANGEBYSCORE" "allTickets" "-Inf" "+Inf"
1576500160.703530 [0 10.1.13.148:52540] "MGET" "bnrnnf14tpl76e14h5a0" "bnrnnf94tpl76e14h5ag" "bnrnnfp4tpl76e14h5b0"
```

Frontendで書き込まれたインデックスキーから値の範囲を指定してチケット情報を取得しています。
ここでは検索条件がないので、allTicketsの全てを取得しています。
このデータがマッチングロジックに渡されます。

```text:マッチ提案
1576500165.703594 [0 10.1.13.147:47004] "MULTI"
1576500165.703622 [0 10.1.13.147:47004] "ZADD" "proposed_ticket_ids" "1576500165703326682" "bnrnnfp4tpl76e14h5b0"
1576500165.703640 [0 10.1.13.147:47004] "ZADD" "proposed_ticket_ids" "1576500165703326682" "bnrnnf14tpl76e14h5a0"
1576500165.703664 [0 10.1.13.147:47004] "EXEC"
```

マッチングロジックから提案されたチケットIDが検索から除外されるように追加されています。

### 割り当て

マッチが完了したら、AssignTicketsでゲームサーバーの割り当てを行います。
ここではランダムなアドレスを設定しています。

```go:director.go
	s.Status = "Assigning Players"
	ds.Update(s)

	for _, match := range matches {
		ids := []string{}

		for _, t := range match.Tickets {
			ids = append(ids, t.Id)
		}

		req := &pb.AssignTicketsRequest{
			TicketIds: ids,
			Assignment: &pb.Assignment{
				Connection: fmt.Sprintf("%d.%d.%d.%d:2222", rand.Intn(256), rand.Intn(256), rand.Intn(256), rand.Intn(256)),
			},
		}

		resp, err := be.AssignTickets(ds.Ctx, req)
		if err != nil {
			panic(err)
		}

		_ = resp
	}
```

```text:割り当て
1576500165.730838 [0 10.1.13.157:35400] "GET" "bnrnnfp4tpl76e14h5b0"
1576500165.731933 [0 10.1.13.157:35400] "GET" "bnrnnf14tpl76e14h5a0"
1576500165.732428 [0 10.1.13.157:35400] "MULTI"
1576500165.732438 [0 10.1.13.157:35400] "SET" "bnrnnfp4tpl76e14h5b0" "\n\x14bnrnnfp4tpl76e14h5b0\x1a\x16\n\x14156.106.164.233:2222"
1576500165.732448 [0 10.1.13.157:35400] "EXPIRE" "bnrnnfp4tpl76e14h5b0" "43200"
1576500165.732453 [0 10.1.13.157:35400] "EXEC"
1576500165.733721 [0 10.1.13.157:35400] "MULTI"
1576500165.733735 [0 10.1.13.157:35400] "SET" "bnrnnf14tpl76e14h5a0" "\n\x14bnrnnf14tpl76e14h5a0\x1a\x16\n\x14156.106.164.233:2222"
1576500165.733769 [0 10.1.13.157:35400] "EXPIRE" "bnrnnf14tpl76e14h5a0" "43200"
1576500165.733779 [0 10.1.13.157:35400] "EXEC"
```

チケットIDキーに対して、割り当て情報を設定を行っています。

```text:インデックス削除
1576500165.735677 [0 10.1.13.157:35396] "MULTI"
1576500165.735686 [0 10.1.13.157:35396] "EXEC"
1576500165.736289 [0 10.1.13.157:35396] "SMEMBERS" "ic$bnrnnfp4tpl76e14h5b0"
1576500165.736528 [0 10.1.13.157:35396] "MULTI"
1576500165.736541 [0 10.1.13.157:35396] "ZREM" "allTickets" "bnrnnfp4tpl76e14h5b0"
1576500165.736550 [0 10.1.13.157:35396] "DEL" "ic$bnrnnfp4tpl76e14h5b0"
1576500165.736554 [0 10.1.13.157:35396] "EXEC"
1576500165.737618 [0 10.1.13.157:35396] "SMEMBERS" "ic$bnrnnf14tpl76e14h5a0"
1576500165.738435 [0 10.1.13.157:35396] "MULTI"
1576500165.738445 [0 10.1.13.157:35396] "ZREM" "allTickets" "bnrnnf14tpl76e14h5a0"
1576500165.738452 [0 10.1.13.157:35396] "DEL" "ic$bnrnnf14tpl76e14h5a0"
1576500165.738457 [0 10.1.13.157:35396] "EXEC"
1576500165.739455 [0 10.1.13.157:35396] "MULTI"
1576500165.739465 [0 10.1.13.157:35396] "ZREM" "proposed_ticket_ids" "bnrnnfp4tpl76e14h5b0"
1576500165.739472 [0 10.1.13.157:35396] "ZREM" "proposed_ticket_ids" "bnrnnf14tpl76e14h5a0"
1576500165.739477 [0 10.1.13.157:35396] "EXEC"
```

割り当てが完了したチケットが検索対象にならないように、インデックスから削除を行っています。

## 最後に

デモを実際に動かして、簡単にですがデータの流れを追ってみました。
Open Matchの理解に少しでも役立てればと思います。