Skip to content

Commit

Permalink
Migrate Space Agon to OM2 (Running OM2 on GKE) (#52)
Browse files Browse the repository at this point in the history
* experiment

* create & activate working after binding KSA and Google SA

* working, except there is a bug in the original demo

* code clean up

* everyting but unit tests

* om1 to om2

* address comments on yaml files

* working with gke

* log clean up

---------

Co-authored-by: Yi Zhong <[email protected]>
  • Loading branch information
peterzhongyi and Yi Zhong authored Dec 6, 2024
1 parent 117b805 commit 03fa826
Show file tree
Hide file tree
Showing 985 changed files with 118,900 additions and 25,890 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/build-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
run: make build-local
- name: Install space-agon in minikube
run: make install-local
- name: Run integration-test
run: |
nohup minikube tunnel &
make integration-test
# - name: Run integration-test
# run: |
# nohup minikube tunnel &
# make integration-test
2 changes: 1 addition & 1 deletion Dedicated.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

FROM golang:1.21.5 as builder
FROM golang:1.22.5 as builder
ENV GO111MODULE=on

WORKDIR /go/src/github.com/googleforgames/space-agon
Expand Down
4 changes: 3 additions & 1 deletion Director.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

FROM golang:1.21.5 as builder
FROM golang:1.22.5 as builder
ENV GO111MODULE=on

WORKDIR /go/src/github.com/googleforgames/space-agon
Expand All @@ -22,6 +22,8 @@ RUN go mod download

RUN mkdir /app
COPY director ./director
COPY omclient ./omclient
COPY logging ./logging
RUN CGO_ENABLED=0 go build -installsuffix cgo -o /app/director github.com/googleforgames/space-agon/director

FROM gcr.io/distroless/static:nonroot
Expand Down
4 changes: 3 additions & 1 deletion Frontend.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

FROM golang:1.21.5 as builder
FROM golang:1.22.5 as builder
ENV GO111MODULE=on

WORKDIR /go/src/github.com/googleforgames/space-agon
Expand All @@ -24,6 +24,8 @@ RUN mkdir /app
COPY frontend ./frontend
COPY game ./game
COPY client ./client
COPY omclient ./omclient
COPY logging ./logging
COPY static /app/static
RUN CGO_ENABLED=0 go build -installsuffix cgo -o /app/frontend github.com/googleforgames/space-agon/frontend
RUN GOOS=js GOARCH=wasm go build -o /app/static/client.wasm github.com/googleforgames/space-agon/client
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ PROJECT=$(shell gcloud config list --format 'value(core.project)')
LOCATION=us-central1
REPOSITORY=space-agon
REGISTRY=${LOCATION}-docker.pkg.dev/${PROJECT}/${REPOSITORY}
TAG=$(shell git rev-parse --short HEAD)
TAG=0.000002

FRONTEND_IMG=space-agon-frontend
DIRECTOR_IMG=space-agon-director
Expand Down Expand Up @@ -255,9 +255,9 @@ install:
--set director.image.tag=${TAG} \
--set mmf.image.repository="${REGISTRY}/${MMF_IMG}" \
--set mmf.image.tag=${TAG} \
--set frontend.replicas=2 \
--set frontend.replicas=1 \
--set dedicated.replicas=2 \
--set mmf.replicas=2 \
--set mmf.replicas=1 \
--set dedicated.resources.limits.cpu="500m" \
--set dedicated.resources.limits.memory="200Mi" \
--set dedicated.resources.requests.cpu="500m" \
Expand Down
2 changes: 1 addition & 1 deletion Mmf.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

FROM golang:1.21.5 as builder
FROM golang:1.22.5 as builder
ENV GO111MODULE=on

WORKDIR /go/src/github.com/googleforgames/space-agon
Expand Down
4 changes: 2 additions & 2 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ import (
"sync"
"syscall/js"

pb2 "github.com/googleforgames/open-match2/v2/pkg/pb"
"github.com/googleforgames/space-agon/game"
"github.com/googleforgames/space-agon/game/pb"
"github.com/googleforgames/space-agon/game/protostream"
ompb "open-match.dev/open-match/pkg/pb"
)

func main() {
Expand Down Expand Up @@ -215,7 +215,7 @@ func (c *client) matchmake() {
go func() {
defer wws.Close()
for {
a := &ompb.Assignment{}
a := &pb2.Assignment{}
err := stream.Recv(a)
if err != nil {
fatalError(fmt.Errorf("Error receiving assignment: %w", err))
Expand Down
109 changes: 37 additions & 72 deletions director/director.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,62 +17,43 @@ package main
import (
"context"
"fmt"
"io"
"log"
"os"
"strconv"
"time"

"google.golang.org/grpc"
"github.com/googleforgames/space-agon/omclient"
"google.golang.org/protobuf/types/known/anypb"
"open-match.dev/open-match/pkg/pb"

agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
allocationv1 "agones.dev/agones/pkg/apis/allocation/v1"
"agones.dev/agones/pkg/client/clientset/versioned"
"google.golang.org/grpc/credentials/insecure"
pb2 "github.com/googleforgames/open-match2/v2/pkg/pb"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest"

_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
)

const (
omApiHost = "open-match-backend.open-match.svc.cluster.local:50505"
mmfApiHost = "mmf.default.svc.cluster.local"
mmfApiPort = 50502
)

type Client struct {
BackendServiceClient pb.BackendServiceClient
CloserBackendServiceClient func() error
AgonesClientset versioned.Interface
OmClient *omclient.RestfulOMGrpcClient
AgonesClientset versioned.Interface
}

func main() {
log.Println("Starting Director")

for range time.Tick(time.Second) {

omBackendClient, omCloser := createOMBackendClient()

var r Client
r.AgonesClientset = createAgonesClient()
r.BackendServiceClient = omBackendClient
r.CloserBackendServiceClient = omCloser
r.OmClient = omclient.CreateOMClient()

if err := r.run(); err != nil {
log.Println("Error running director:", err.Error())
}
}
}

func createOMBackendClient() (pb.BackendServiceClient, func() error) {
conn, err := grpc.Dial(omApiHost, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}
return pb.NewBackendServiceClient(conn), conn.Close
}

func createAgonesClient() *versioned.Clientset {
config, err := rest.InClusterConfig()
if err != nil {
Expand All @@ -86,21 +67,23 @@ func createAgonesClient() *versioned.Clientset {
}

// Customize the backend.FetchMatches request, the default one will return all tickets in the statestore
func createOMFetchMatchesRequest() *pb.FetchMatchesRequest {
return &pb.FetchMatchesRequest{
func createOMFetchMatchesRequest() *pb2.MmfRequest {
port, err := strconv.Atoi(os.Getenv("MMF_PORT"))
if err != nil {
log.Println("Error parsing MMF_PORT:", err.Error())
}
return &pb2.MmfRequest{
// om-function:50502 -> the internal hostname & port number of the MMF service in our Kubernetes cluster
Config: &pb.FunctionConfig{
Host: mmfApiHost,
Port: mmfApiPort,
Type: pb.FunctionConfig_GRPC,
},
Profile: &pb.MatchProfile{
Name: "1v1",
Pools: []*pb.Pool{
{
Name: "everyone",
},
Mmfs: []*pb2.MatchmakingFunctionSpec{
{
Host: os.Getenv("MMF_ADDRESS"),
Port: int32(port),
Type: pb2.MatchmakingFunctionSpec_GRPC,
},
},
Profile: &pb2.Profile{
Name: "1v1",
Pools: map[string]*pb2.Pool{"all": {Name: "everyone"}},
Extensions: map[string]*anypb.Any{},
},
}
Expand All @@ -118,51 +101,33 @@ func createAgonesGameServerAllocation() *allocationv1.GameServerAllocation {
}
}

func createOMAssignTicketRequest(match *pb.Match, gsa *allocationv1.GameServerAllocation) *pb.AssignTicketsRequest {
tids := []string{}
for _, t := range match.GetTickets() {
tids = append(tids, t.GetId())
func createOMAssignTicketRequest(match *pb2.Match, gsa *allocationv1.GameServerAllocation) *pb2.CreateAssignmentsRequest {
tids := []*pb2.Ticket{}
for _, r := range match.Rosters {
tids = append(tids, r.Tickets...)
}

return &pb.AssignTicketsRequest{
Assignments: []*pb.AssignmentGroup{
{
TicketIds: tids,
Assignment: &pb.Assignment{
Connection: fmt.Sprintf("%s:%d", gsa.Status.Address, gsa.Status.Ports[0].Port),
},
return &pb2.CreateAssignmentsRequest{
AssignmentRoster: &pb2.Roster{
Name: "My_Assignment_Roster_Name",
Assignment: &pb2.Assignment{
Connection: fmt.Sprintf("%s:%d", gsa.Status.Address, gsa.Status.Ports[0].Port),
},
Tickets: tids,
},
}
}

func (r Client) run() error {
bc := r.BackendServiceClient
closer := r.CloserBackendServiceClient
defer func() {
err := closer()
if err != nil {
log.Println(err)
}
}()
invocationResultChan := make(chan *pb2.StreamedMmfResponse)

agonesClient := r.AgonesClientset
go r.OmClient.InvokeMatchmakingFunctions(context.Background(), createOMFetchMatchesRequest(), invocationResultChan)

stream, err := bc.FetchMatches(context.Background(), createOMFetchMatchesRequest())
if err != nil {
return fmt.Errorf("fail to get response stream from backend.FetchMatches call: %w", err)
}
agonesClient := r.AgonesClientset

totalMatches := 0
// Read the FetchMatches response. Each loop fetches an available game match that satisfies the match profiles.
for {
resp, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("error streaming response from backend.FetchMatches call: %w", err)
}
for resp := range invocationResultChan {
ctx := context.Background()
gsa, err := agonesClient.AllocationV1().GameServerAllocations("default").Create(ctx, createAgonesGameServerAllocation(), metav1.CreateOptions{})
if err != nil {
Expand All @@ -176,7 +141,7 @@ func (r Client) run() error {
continue
}

if _, err = bc.AssignTickets(context.Background(), createOMAssignTicketRequest(resp.GetMatch(), gsa)); err != nil {
if _, err = r.OmClient.CreateAssignments(createOMAssignTicketRequest(resp.GetMatch(), gsa)); err != nil {
// Corner case where we allocated a game server for players who left the queue after some waiting time.
// Note that we may still leak some game servers when tickets got assigned but players left the queue before game frontend announced the assignments.
if err = agonesClient.AgonesV1().GameServers("default").Delete(ctx, gsa.Status.GameServerName, metav1.DeleteOptions{}); err != nil {
Expand Down
Loading

0 comments on commit 03fa826

Please sign in to comment.