Compare commits

...

141 Commits

Author SHA1 Message Date
b6107c5640 A small csharp project intended to contribute some design ideas around synchronizing contexts and in-memory data stores 2019-06-05 00:53:38 -07:00
3e61359f05 Create third party folder with grpc-gateway *.proto dependencies (#460)
* Create third party folder with grpc-gateway *.proto dependencies

* Enable automated third_party download

* third_party
2019-06-04 17:34:04 -07:00
8275ed76c5 Consolidate statstore/public.New signature (#482)
* Consolidate statestore.New signature

* Fix bad merge

* Fix bad merge
2019-06-04 17:22:55 -07:00
e8b2525262 Distinguish between example and demo. (#488)
There will be many example MMFs, but we specifically want an runnable demo. (the demo is an example, but not all examples are the demo.)

I change the install/helm from example to demo, as it now specifically only installs the demo. Changes in the Makefile to reflect that.
I also add new make commands to build and push the demo images. Currently it only contains the example mmf, but it will in the near future contain the demo driver image.

Tested = Created a GKE cluster and installed via the make commands.
2019-06-04 16:52:55 -07:00
3517b7725c Add Kubernetes health checks. (#469) 2019-06-04 16:01:30 -07:00
6cd521abf7 Remove TestServerBinding from component tests (#485) 2019-06-04 15:18:29 -07:00
924fccfeb3 Update to new respository location (#483)
CI is broken.
2019-06-04 12:51:43 -07:00
c17ca7a10c Update GetAssignment and UpdateAssignment methods to meet design need (#477)
* Update assignment methods

* nolint on exponential backoff strat
2019-06-04 11:28:17 -07:00
b11863071f Revert presubmit (#480) 2019-06-04 11:15:14 -07:00
20dbcea99f Fix short sha tags in docker builds. (#479) 2019-06-04 11:02:20 -07:00
13505956a0 Enable golangci in presubmit (#475) 2019-06-03 19:57:02 -07:00
3d04025860 Have sample MMF create simple 1v1 matches (#438)
The previous MMF required a weird behavior where you needed to set tickets in rosters
to be overridden. It would also break if there weren't enough tickets to fill the rosters. This
is a simpler example which takes tickets from the pool and assigns them into 1v1 matches.
2019-06-03 17:51:20 -07:00
d0f7f2c4d3 Implemented backend rest config logic (#459)
* Implemented backend rest config logic

* Remove unncessary logs and let logics return rpc status error

* Fix chatty bot

* Fix bad merge
2019-05-31 17:12:03 -07:00
272e7642b1 Move back to helm2 and adjust cluster size again. (#472) 2019-05-31 15:38:27 -07:00
f3f80a70bd Reorganize backend service and e2e test for incoming rest config logic (#458)
* Reorganize backend service and e2e test for incoming rest config logic
2019-05-31 12:57:50 -07:00
80bcd9487f Make backoff strategy configurable (#467) 2019-05-31 11:32:26 -07:00
2e25daf474 Increase the size of the default cluster. We are hitting vCPU limits (#464) 2019-05-30 17:51:03 -07:00
9fe32eef96 Implements frontend and backend Assignments method with tests (#463)
* Implements backend AssignTickets method

* implmenet backend get assignment method with tests

* Fix test comments

* Remove redundant log

* Go mod tidy
2019-05-30 20:18:17 -04:00
0446159872 Modify FunctionConfig to indicate mmf server type (#448)
* Make backend service supports secure mmf server
2019-05-30 19:59:29 -04:00
2ef8614687 Switch to Helm 3-alpha1 (#453) 2019-05-30 16:03:28 -07:00
de8279dfe0 Add server/client test helpers and a basic test. (#462) 2019-05-30 15:42:39 -07:00
8fedc2900f Implements evaluator harness and a default example (#449)
* Implements evaluator harness and a default example
2019-05-30 18:17:35 -04:00
0f95adce20 Update copyright headers (#461) 2019-05-30 14:53:02 -07:00
4f851094a0 Fix some subtle TLS bugs and remove clientCredentialsFromFileData (#457) 2019-05-30 06:37:01 -07:00
9cae854771 Enable most of the golangci checks and fix internal/set tests (#416)
* Enable most of the golangci checks and fix internal/set tests
2019-05-29 18:33:54 -04:00
603089f404 Add Binary Authorization commands. (#451) 2019-05-29 14:59:03 -07:00
d024b46487 Enable GKE Autoscaling (#455) 2019-05-29 14:22:33 -07:00
a2616870c7 Splitup host and prefix variables (#447) 2019-05-29 14:27:37 -04:00
6d8b516026 Expose minimatch config to test context (#445) 2019-05-29 14:13:21 -04:00
b4d3e84e3d Update tools, help, and scope some global vars. (#426) 2019-05-29 07:48:21 -07:00
6b370f56c8 E2E tests for Open Match using Minimatch. (#440)
* E2E tests for Open Match using Minimatch.

The test binary starts all core Open Match services and a sample MMF in
proc. It then uses some test data to create tickets, query for Pools and
fetch matches and validate that the pools and matches have expected
tickets.
2019-05-25 00:23:01 -07:00
d5da3d16b7 Fix minor issues that were encountered in an E2E test case for generating matches. (#437)
* Fix minor issues that were encountered in an E2E test case for generating matches. Here are the issues this change fixes:

1. Add error check to avoid accessing failed connection in tcp net listener
2. Divide by zero error in the redis state storage library if page size is 0.
3. Ability to support null hostname when connecting to mmlogic client in match function harness.
4. Set min / max page size limits to initialize correctly if page size is set to unsupported values.

* review changes
2019-05-22 22:44:03 -07:00
31469bb0f9 Modify the Function Configuration proto to improve code readability. (#435)
Current proto names both the variable bearing the function config - and the config proto as grpc, due to which proto generation generates structures representing the variable and the type as differing by an '_'. Code using this current style turns out confusing and hard to read. Renaming the type different from the variable to improve code readability.
2019-05-21 22:52:07 -07:00
e8adc57f76 Add input validation to FetchMatches. The call now fails with (#434)
InvalidArgument if match configuration is missing or unsupported.
2019-05-21 14:36:24 -07:00
0da8d0d221 Fix 'make delete-chart' - use ignore-not-found properly (#433)
Without '=true', it complains that crd is not a supported kubectl command (because it thinks delete is an arg to ignore-not-found.)
2019-05-21 10:15:27 -07:00
08d9210588 Fix: Frontend config specifies grpcport properly. (#432)
It appears the code reads this properly already. It's just that the missing config probably defaulted to 0, which when assigning a port works.
2019-05-20 16:09:11 -07:00
8882d8c9a1 Fix mmf harness reading port from kub config (#431) 2019-05-20 15:40:23 -07:00
6c65e924ec Use proto3's struct for properties, not string. (#430)
This makes the API more usuable for both json clients (who no longer
have to encode json into a string and put that into their json) and also
for proto clients (who no longer have to use json...). When converted
to json, struct will be encoded directly into an object, which is much
more convenient.

The majority of the rest of this change is fixing tickets in tests.

This removes the dependancy on the json parser that was used for reading
from properties.

I also added a test on indexing and filtering tickets from the state
store, because I can't help myself and I have a problem.
2019-05-20 13:33:55 -07:00
beba937ac5 Add a new Evaluator service to Open Match core components to perform Match evaluation. (#424)
Evaluator component synchronizes all the generated proposals, evaluates
them for quality, deduplicates proposals and generates result
matches. Backend service can scale with number of backend requests but
the evaluator acts as the single point aggregator for results. Hence it
requires to be on a separate service.

This change only introduces the scaffolding for the evaluator, tying
adding it as a core component to Open Match build, deployment and other
tooling. This change does not actually wire the evaluator up into the
match generation flow. The change that adds the core evaluator logic
will follow.
2019-05-17 16:24:18 -07:00
caa755272b Implement FetchMatches on Backend Service (#425) 2019-05-17 16:04:16 -07:00
ea60386fa0 Implement clientwrapper for harness and testing (#415)
* Implement clientwrapper for harness and testing

* Disable clientAuthAndVerify on tlsServer

* Remove stale temp file codes from clients_test.go
2019-05-17 15:31:55 -07:00
9000ae8de4 Have redis filter query return full tickets (#429)
* Have redis filter query and return full tickets

* break out paging logic from redis filter

* Per code review, return grpc statuses
2019-05-17 15:21:23 -07:00
4e2b64722f Splits up stress test utility codes and implements mmlogic stress test (#419) 2019-05-17 12:27:50 -07:00
4c0f24217f Add validation for markdown links (#428) 2019-05-17 09:20:03 -07:00
872b7be6a5 Fixing URL for getting started guide. (#427) 2019-05-17 08:52:00 -07:00
53f2ee208f Proto changes for implementing Backend Service (#421) 2019-05-16 13:49:32 -07:00
b5eaf153e8 Implement Frontend Service Create / Get / Delete Ticket (#417) 2019-05-15 22:39:26 -07:00
d3c7eb2000 Rename serving functions and params with 'Server' prefix (#418) 2019-05-15 14:29:29 -07:00
23243e2815 Add SwaggerUI to website (#413) 2019-05-15 09:02:08 -07:00
3be97908b2 Add make targets for creating TLS certificates. (#410) 2019-05-15 08:28:43 -07:00
5892f81214 Implement the Mmlogic Service (#414) 2019-05-14 18:15:50 -07:00
40892c9b2e Add option to serve with a trusted CA cert. (#409) 2019-05-14 17:18:26 -07:00
9691d3f001 Refactor serving package to move to serving/rpc path. (#400)
* move serving codes to rpc directory to hide tls util methods

* nolint on unused tls util codes
2019-05-14 15:36:32 -07:00
9808066375 Move open match core components from future folders to actual destination. (#412)
* Remove future
2019-05-14 14:34:19 -07:00
534716eef4 Fix golangci errors (#405)
* Fix golangci errors
2019-05-14 13:35:54 -07:00
ece4a602d0 Move binaries to cmd/ (#406) 2019-05-14 13:04:08 -07:00
8eb72d98b2 Delete old Open Match (#408) 2019-05-14 11:04:52 -07:00
6da8a34b67 Ignore errors from make delete-kind-cluster (#407) 2019-05-14 10:37:39 -07:00
b03189e34c Delete evaluator (#404) 2019-05-14 06:40:13 -07:00
9766871a87 new mmf service impl (#403)
* mmf service impl
2019-05-13 19:16:08 -07:00
4eac4cb29a New MMF harness Makefile and skeletons (#402)
* New MMF harness Makefile and skeletons

* Not simple
2019-05-13 16:28:10 -07:00
439286523d Remove old mmf codes (#387)
* Remove old mmf codes

* cleanup makefile

* Cleanup cloudbuild

* makes cloudbuild great again

* Delete unmarshal.go

* Delete unmarshal_test.go
2019-05-13 15:46:37 -07:00
e0058c7c08 Get started guide (#395)
* Get started guide and fix development guide typo
2019-05-13 15:08:58 -07:00
ec40f26e62 Fix make all denpendency (#388) 2019-05-13 11:37:30 -07:00
17134f0a40 Break up tls util codes (#399)
* Break up tls util codes
2019-05-13 11:16:18 -07:00
add2464b33 Basic experimental knative instructions (#398) 2019-05-13 10:58:17 -07:00
b72b4f9b54 Redis implementation for State Storage methods for Tickets (#384) 2019-05-10 16:03:56 -07:00
abdc3aca28 Clean up unused bindata (#383) 2019-05-10 10:43:26 -07:00
3ab724e848 Initialize state storage in Frontend, Backend and MMLogic services (#377) 2019-05-09 23:38:03 -07:00
3c8d0ce1b0 Fix the URL for install yamls. (#382) 2019-05-09 15:58:11 -07:00
c0166e3176 Refactor protos to match CRUD operations (#365) 2019-05-09 15:05:03 -07:00
3623adb90e Fix URLs, Post Submit, add gofmt to presubmit (#381) 2019-05-09 11:12:31 -07:00
fba1bcf445 Fix post commit (#379) 2019-05-09 06:57:17 -07:00
fdd865200a Set CORS policy on open-match.dev (#373) 2019-05-08 14:40:27 -07:00
b0fc8f261f Remove helm chart autopush (#378) 2019-05-08 14:11:08 -07:00
bdd3503d80 Add post commit for website auto push. (#370) 2019-05-08 13:25:20 -07:00
81fd2bab83 Add abstraction to storage layer to decouple Redis from Open Match Services (#367) 2019-05-08 12:54:34 -07:00
212a416789 Add Monitoring Configuration to Open Match (#362) 2019-05-08 12:13:25 -07:00
2425e28129 Improve development guide and remove old user guide. (#371) 2019-05-08 11:33:29 -07:00
3993a2f584 Delete old open match. gRPC harness will remain for now because it does not have an equivalent yet. (#351) 2019-05-08 10:21:32 -07:00
05cb4e503f Fix broken link. (#375) 2019-05-08 09:36:18 -07:00
1cf11e7d81 Add image-spec annotions (#359) 2019-05-07 19:40:28 -07:00
1985ecefed Fix issues with website before launch (#360) (#364) 2019-05-07 19:17:57 -07:00
b7ebb60325 Update helm chart dependencies, and add Jaeger for tracing. (#358) 2019-05-07 16:04:38 -07:00
e4651d9382 Add minimatch binary to gitignore (#366) 2019-05-07 15:09:37 -07:00
04a574688a Monitoring package for Open Match (#363) 2019-05-07 13:49:50 -07:00
d4a901fc71 Update frontend stress to use new API. (#361) 2019-05-07 13:27:50 -07:00
5de79f90cf Fix mmlogic readiness probe. (#357) 2019-05-07 11:20:34 -07:00
e42c8a0232 Add KinD support for OM deployments. (#355) 2019-05-07 11:03:26 -07:00
1503ffae3a Add make proxy*, update tools, and cleanup make output. (#356) 2019-05-07 10:42:50 -07:00
a842da5563 Download includes before using protoc tools. (#349) 2019-05-07 07:07:08 -07:00
c3d6efef72 Use golang vanity url: open-match.dev/open-match (#114) (#321) 2019-05-06 07:57:43 -07:00
0516ab0800 Minimatch for 0.6.0 (#346) 2019-05-06 07:33:47 -07:00
668bfd6104 updating release documentation and small edits to README (#340)
* updating release documentation and small edits to README

* Update release.md

* Update README.md
2019-05-03 19:03:35 -07:00
ef933ed6ef Add stubbed abstracted Redis client. (#345)
* Add stubbed abstracted Redis client.

* Make this work
2019-05-03 15:01:32 -07:00
3ee24e3f28 Add documentation on how to update the docs. (#344) 2019-05-03 11:32:59 -07:00
d0bd794a61 Move the future/fake_frontend to use the future/pb. (#339) 2019-05-03 10:58:41 -07:00
37bbf470de Replace helm chart for the new binaries. (#342) 2019-05-03 10:34:14 -07:00
412cb8557a Move all deprecated Makefile targets to the bottom. (#341) 2019-05-03 09:31:29 -07:00
38e81a9fd1 Revamp the website to include the basics and prepare for launch. (#334) 2019-05-02 15:59:48 -07:00
cb24baf107 Add gRPC/TLS and HTTPS serving support. (#330) 2019-05-02 15:12:13 -07:00
c6f6526823 Add top level Swagger annotations for API. (#335) 2019-05-02 13:28:31 -07:00
41e441050f Add main() for new binaries and wire up to CI (#336) 2019-05-02 12:44:29 -07:00
235e25c748 Remobve 404ing pages. (#331) 2019-05-01 20:19:25 -07:00
93ca5e885c make test now does coverage (#328) 2019-05-01 19:59:18 -07:00
5d67fb1548 Add Root CA support to certgen. (#273) (#308) 2019-05-01 17:35:55 -07:00
faa6e17607 Rename CreateTicketsResponse to CreateTicketResponse for consistency. (#332) 2019-05-01 14:53:08 -07:00
6a0c648a8f Add missing copyright headers and godoc package comments. (#329) 2019-05-01 14:23:35 -07:00
8516e3e036 Frontendapi load testing impl (#290)
* Frontendapi Stress Tests
2019-05-01 11:55:09 -07:00
e476078b9f Auto-generate new protobufs and APIs. (#325) 2019-04-30 14:24:21 -07:00
0d405df761 Fix up some lint errors. (#326) 2019-04-30 12:53:58 -07:00
06c1208ab2 Remove log alias from logrus imports (#327) 2019-04-30 12:38:15 -07:00
af335032a8 Add protoc-gen-swagger proto options to includes. (#324) 2019-04-30 11:34:36 -07:00
a8be8afce2 Add insecure gRPC serving to future/. (#316) 2019-04-30 08:46:42 -07:00
e524121b4b Clearify in release issue to use release notes drafted in rc1 of a re… (#323)
* Clearify in release issue to use release notes drafted in rc1 of a release.  Also improve wording around instructions to create the release.
2019-04-29 17:45:17 -07:00
871abeee69 Initial protos for Open Match 0.6 (#322) 2019-04-29 16:39:49 -07:00
b9af86b829 Allow creating global loggers. (#317) 2019-04-29 16:15:14 -07:00
6a9572c416 Fix the trailing slash issue in Makefile (#319) 2019-04-29 15:44:55 -07:00
636eb07869 Use n1-highcpu-32 machine and cache base image for 2 days in CI (#231) (#301) 2019-04-29 15:04:38 -07:00
d56c983c17 Add readiness probe and remove redis sanity check 2019-04-29 14:02:32 -07:00
a8e857c0ba Introduce internal/future/ directory with it's first file, fake_frontend.go. (#306) (#307) 2019-04-29 12:55:42 -07:00
75da6e1f4a Update master to use 0.0.0-dev for version (#296) 2019-04-29 10:41:54 -07:00
e1fba5c1e8 Update the vanity url to open-match.dev/open-match (#311) 2019-04-29 10:04:40 -07:00
d9911bdfdd golangci support (#242) 2019-04-29 09:36:29 -07:00
175293fdf9 Consolidate build and push docker images for better CPU utilization (#302) 2019-04-29 07:00:41 -07:00
01407fbcad Rephrase makefile set-redis-password command (#313) 2019-04-29 06:29:11 -07:00
edad339a76 Make matchmaker_config.yaml a part of Helm chart config (#204)
* move config pkg under internal/;matchmaker_config.yaml is a part of Helm chart config now

* ignore data race in some tests
2019-04-25 21:51:20 -07:00
c57c841dfc Add ListenerHolder.AddrString() to avoid bad IPv6 assumptions. (#299) 2019-04-25 21:31:15 -07:00
54dc0e0518 Update gcloud.md steps to work with free tier (#294)
While following documentation to create a cluster for Open Match using GCP, the command line example says to create using n1-standard-4 machine type. 
It seems like it is not possible when on a free tier, at least on the default settings: 

`C:\Program Files (x86)\Google\Cloud SDK>gcloud container clusters create --machine-type n1-standard-4 open-match-dev-cluster --zone us-west1-a --tags open-match`

`ERROR: (gcloud.container.clusters.create) ResponseError: code=403, message=Insufficient regional quota to satisfy request: resource "CPUS": request requires '12.0' and is short '4.0'. project has a quota of '8.0' with '8.0' available. View and manage quotas at https://console.cloud.google.com/iam-admin/quotas?usage=USED&project=XXXXX`

To remove as many potential hurdles for people new to GCP, I would suggest to replace by a n1-standard-2 which works straight away, as I expect open-match can work with it?
2019-04-25 09:07:24 -07:00
fa4e8887d0 Update README.md to add Windows batch line version (#295)
* Fixing a few typos
* On Windows shell, you can't use backticks to catch the result of a program call as a string, I believe the standard way to do that is to use the **for** command to catch the result in a variable and then use it. I included that version in the "Deploying Open Match" section, as the current version does not work on Windows.
2019-04-25 08:12:16 -07:00
8384cb00b2 Self-Certificate generation for Open Match. (#274) 2019-04-25 07:40:35 -07:00
b9502a59a0 Fix REST proxy and added proxy health check tests (#275)
* Add proxy tests with swagger and healthcheck handlers

* Block grpcserver Start() until fully initialized

* Serve each .swagger.json file on its corresponding REST endpont

* Resolve comments

* Add waitgroup to fully initialize the grpcserver
2019-04-24 15:30:31 -07:00
139d345915 Fix win64 and binary dependencies. (#287) 2019-04-24 13:36:03 -07:00
5ea5b29af4 Make release issue a github issue template (#280) 2019-04-24 12:34:50 -07:00
812afb2d06 Backend Client should keep generating matches, displaying failures if any (#285) 2019-04-24 10:51:29 -07:00
cc82527eb5 Update documentation to reflect 0.5.0 changes. (#283) 2019-04-24 09:38:28 -07:00
80b20623fb Include cloudbuild in places version needs to be set. Specify rc for all versions (#278) 2019-04-23 17:05:28 -07:00
a270eab4b4 Update release notes based on feedback. (#269) (#276) 2019-04-23 16:07:14 -07:00
36f7dcc242 Makefile for windows (#272) 2019-04-23 15:22:13 -07:00
a4706cbb73 Automatically publish development.open-match.dev on Post Commit (#201) (#263) 2019-04-23 13:37:51 -07:00
c09cc8e27f make clean now deletes build/ (#261) 2019-04-23 11:15:35 -07:00
342 changed files with 15650 additions and 17525 deletions

View File

@ -1,3 +1,17 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Binaries for programs and plugins
*.exe
*.exe~
@ -16,6 +30,10 @@
*swo
*~
# Load testing residuals
test/stress/*.csv
test/stress/__pycache__
# Ping data files
*.ping
*.pings
@ -83,14 +101,17 @@ install/yaml/
# Temp Directories
tmp/
# Compiled Binaries
# Open Match Binaries
cmd/backend/backend
cmd/frontend/frontend
cmd/mmlogic/mmlogic
cmd/evaluator/evaluator
cmd/minimatch/minimatch
cmd/backendapi/backendapi
cmd/frontendapi/frontendapi
cmd/mmlogicapi/mmlogicapi
examples/backendclient/backendclient
examples/evaluators/golang/serving/serving
examples/functions/golang/grpc-serving/grpc-serving
test/cmd/clientloadgen/clientloadgen
test/cmd/frontendclient/frontendclient
tools/certgen/certgen
examples/functions/golang/simple/simple
# Open Match Build Directory
build/
# Secrets Directories
install/helm/open-match/secrets/

View File

@ -1,3 +1,17 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This file specifies files that are *not* uploaded to Google Cloud Platform
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style

160
.github/ISSUE_TEMPLATE/release.md vendored Normal file
View File

@ -0,0 +1,160 @@
---
name: release
about: Instructions and checklist for creating a release.
title: 'Release X.Y.Z-rc.N'
labels: kind/release
assignees: ''
---
# Open Match Release Process
Follow these instructions to create an Open Match release. The output of the
release process is new images and new configuration.
## Getting setup
*note: the commands below are pasted from the 0.5 release. make the necessary
changes to match your naming & environment.*
The Git flow for pushing a new release is similar to the development process
but there are some small differences.
**1. Clone your fork of the Open Match repository.**
```shell
git clone git@github.com:afeddersen/open-match.git
```
**2. Move into the new open-match directory.**
```shell
cd open-match
```
**3. Configure a remote that points to the upstream repository. This is required to sync changes you make in a fork with the original repository. Note: Upstream is the gatekeeper of the project or the source of truth to which you wish to contribute.**
```shell
git remote add upstream https://github.com/googleforgames/open-match.git
```
**3. Fetch the branches and their respective commits from the upstream repo.**
```shell
git fetch upstream
```
**4. Create a local release branch that tracks upstream and check it out.**
```shell
git checkout -b release-0.5 upstream/release-0.5
```
## Releases & Versions
Open Match uses Semantic Versioning 2.0.0. If you're not familiar please
see the documentation - [https://semver.org/](https://semver.org/).
Full Release / Stable Release:
* The final software product. Stable, reliable, etc...
* Naming example: 1.0.0
Release Candidate (RC):
* A release candidate (RC) is a version with the potential to be the final
product but it hasn't validated by automated and/or manual tests.
* Naming example: 1.0.0-rc.1
Hot Fixes:
* Code developed to correct a major software bug or fault
that's been discovered after the full release.
* Naming example: 1.0.1
# Detailed Instructions
## Find and replace
Below this point you will see {version} used as a placeholder for future
releases. Find {version} and replace with the current release (e.g. 0.5.0)
## Create a release branch in the upstream repository
**Note: This step is performed by the person who starts the release. It is
only required once.**
- [ ] Create the branch in the **upstream** repository. It should be named
release-X.Y. Example: release-0.5. At this point there's effectively a code
freeze for this version and all work on master will be included in a future
version. If you're on the branch that you created in the *getting setup*
section above you should be able to push upstream.
```shell
git push origin release-0.5
```
- [ ] Announce a PR freeze on release-X.Y branch on [open-match-discuss@](mailing-list-post).
- [ ] Open the [`Makefile`](makefile-version) and change BASE_VERSION entry.
- [ ] Open the [`install/helm/open-match/Chart.yaml`](om-chart-yaml-version) and [`install/helm/open-match-example/Chart.yaml`](om-example-chart-yaml-version) and change the `appVersion` and `version` entries.
- [ ] Open the [`install/helm/open-match/values.yaml`](om-values-yaml-version) and [`install/helm/open-match-example/values.yaml`](om-example-values-yaml-version) and change the `tag` entries.
- [ ] Open the [`site/config.toml`] and change the `release_branch` and `release_version` entries.
- [ ] Open the [`cloudbuild.yaml`] and change the `_OM_VERSION` entry.
- [ ] Run `make clean release`
- [ ] There might be additional references to the old version but be careful not to change it for places that have it for historical purposes.
- [ ] Create a PR with the changes and include the release candidate name.
- [ ] Merge your changes once the PR is approved.
## Complete Milestone
**Note: This step is performed by the person who starts the release. It is
only required once.**
- [ ] Create the next [version milestone](https://github.com/googleforgames/open-match/milestones) and use [semantic versioning](https://semver.org/) when naming it to be consistent with the [Go community](https://blog.golang.org/versioning-proposal).
- [ ] Create a *draft* [release](https://github.com/googleforgames/open-match/releases).
- [ ] Use the [release template](https://github.com/googleforgames/open-match/blob/master/docs/governance/templates/release.md)
- [ ] `Tag` = v{version}. Example: v0.5.0. Append -rc.# for release candidates. Example: v0.5.0-rc.1.
- [ ] `Target` = release-X.Y. Example: release-0.5.
- [ ] `Release Title` = `Tag`
- [ ] `Write` section will contain the contents from the [release template](https://github.com/googleforgames/open-match/blob/master/docs/governance/templates/release.md).
- [ ] Add the milestone to all PRs and issues that were merged since the last milestone. Look at the [releases page](https://github.com/googleforgames/open-match/releases) and look for the "X commits to master since this release" for the diff.
- [ ] Review all [milestone-less closed issues](https://github.com/googleforgames/open-match/issues?q=is%3Aissue+is%3Aclosed+no%3Amilestone) and assign the appropriate milestone.
- [ ] Review all [issues in milestone](https://github.com/googleforgames/open-match/milestones) for proper [labels](https://github.com/googleforgames/open-match/labels) (ex: area/build).
- [ ] Review all [milestone-less closed PRs](https://github.com/googleforgames/open-match/pulls?q=is%3Apr+is%3Aclosed+no%3Amilestone) and assign the appropriate milestone.
- [ ] Review all [PRs in milestone](https://github.com/googleforgames/open-match/milestones) for proper [labels](https://github.com/googleforgames/open-match/labels) (ex: area/build).
- [ ] View all open entries in milestone and move them to a future milestone if they aren't getting closed in time. https://github.com/googleforgames/open-match/milestones/v{version}
- [ ] Review all closed PRs against the milestone. Put the user visible changes into the release notes using the suggested format. https://github.com/googleforgames/open-match/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aclosed+is%3Amerged+milestone%3Av{version}
- [ ] Review all closed issues against the milestone. Put the user visible changes into the release notes using the suggested format. https://github.com/googleforgames/open-match/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aclosed+milestone%3Av{version}
- [ ] Verify the [milestone](https://github.com/googleforgames/open-match/milestones) is effectively 100% at this point with the exception of the release issue itself.
TODO: Add guidelines for labeling issues.
## Build Artifacts
- [ ] Go to [Cloud Build](https://pantheon.corp.google.com/cloud-build/triggers?project=open-match-build), under Post Submit click "Run Trigger".
- [ ] Go to the History section and find the "Post Submit" build that's running. Wait for it to go Green. If it's red, fix error repeat this section. Take note of the docker image version tag for next step. Example: 0.5.0-a4706cb.
- [ ] Run `./docs/governance/templates/release.sh {source version tag} {version}` to copy the images to open-match-public-images.
- [ ] If this is a new minor version in the newest major version then run `./docs/governance/templates/release.sh {source version tag} latest`.
- [ ] Copy the files from `build/release/` generated from `make release` to the release draft you created. You can drag and drop the files using the Github UI.
- [ ] Run `make REGISTRY=gcr.io/open-match-public-images TAG={version} delete-gke-cluster create-gke-cluster push-helm sleep-10 install-chart install-demo-chart` and verify that the demo runs correctly.
- [ ] Run `make delete-gke-cluster create-gke-cluster` and run through the instructions under the [README](readme-deploy), verify the pods are healthy. You'll need to adjust the path to the `build/release/install.yaml` and `build/release/install-demo.yaml` in your local clone since you haven't published them yet.
- [ ] Open the [`README.md`](readme-deploy) update the version references and submit. (Release candidates can ignore this step.)
- [ ] Publish the [Release](om-release) in Github.
## Announce
- [ ] Send an email to the [mailing list](mailing-list-post) with the release details (copy-paste the release blog post)
- [ ] Send a chat on the [Slack channel](om-slack). "Open Match {version} has been released! Check it out at {release url}."
[om-slack]: https://open-match.slack.com/
[mailing-list-post]: https://groups.google.com/forum/#!newtopic/open-match-discuss
[release-template]: https://github.com/googleforgames/open-match/blob/master/docs/governance/templates/release.md
[makefile-version]: https://github.com/googleforgames/open-match/blob/master/Makefile#L53
[om-example-chart-yaml-version]: https://github.com/googleforgames/open-match/blob/master/install/helm/open-match/Chart.yaml#L16
[om-example-values-yaml-version]: https://github.com/googleforgames/open-match/blob/master/install/helm/open-match/values.yaml#L16
[om-example-chart-yaml-version]: https://github.com/googleforgames/open-match/blob/master/install/helm/open-match-example/Chart.yaml#L16
[om-example-values-yaml-version]: https://github.com/googleforgames/open-match/blob/master/install/helm/open-match-example/values.yaml#L16
[om-release]: https://github.com/googleforgames/open-match/releases/new
[readme-deploy]: https://github.com/googleforgames/open-match/blob/master/README.md#deploy-to-kubernetes

37
.gitignore vendored
View File

@ -1,3 +1,17 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Binaries for programs and plugins
*.exe
*.exe~
@ -16,6 +30,10 @@
*swo
*~
# Load testing residuals
test/stress/*.csv
test/stress/__pycache__
# Ping data files
*.ping
*.pings
@ -83,13 +101,14 @@ install/yaml/
# Temp Directories
tmp/
# Compiled Binaries
# Open Match Binaries
cmd/backend/backend
cmd/frontend/frontend
cmd/mmlogic/mmlogic
cmd/evaluator/evaluator
cmd/minimatch/minimatch
cmd/backendapi/backendapi
cmd/frontendapi/frontendapi
cmd/mmlogicapi/mmlogicapi
examples/backendclient/backendclient
examples/evaluators/golang/serving/serving
examples/functions/golang/grpc-serving/grpc-serving
test/cmd/clientloadgen/clientloadgen
test/cmd/frontendclient/frontendclient
tools/certgen/certgen
examples/functions/golang/simple/simple
# Secrets Directories
install/helm/open-match/secrets/

248
.golangci.yaml Normal file
View File

@ -0,0 +1,248 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This file contains all available configuration options
# with their default values.
# https://github.com/golangci/golangci-lint#config-file
# options for analysis running
run:
# default concurrency is a available CPU number
concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 5m
# exit code when at least one issue was found, default is 1
issues-exit-code: 1
# include test files or not, default is true
tests: true
# list of build tags, all linters use it. Default is empty list.
build-tags:
# which dirs to skip: they won't be analyzed;
# can use regexp here: generated.*, regexp is applied on full path;
# default value is empty list, but next dirs are always skipped independently
# from this option's value:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
skip-dirs:
# which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is
# no need to include all autogenerated files, we confidently recognize
# autogenerated files. If it's not please let us know.
skip-files:
# output configuration options
output:
# colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number"
format: colored-line-number
# print lines of code with issue, default is true
print-issued-lines: true
# print linter name in the end of issue text, default is true
print-linter-name: true
# all available settings of specific linters
linters-settings:
errcheck:
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: true
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: true
govet:
# report about shadowed variables
check-shadowing: false
# settings per analyzer
settings:
printf: # analyzer name, run `go tool vet help` to see all analyzers
funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
golint:
# minimal confidence for issues, default is 0.8
min-confidence: 0.8
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
gocyclo:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 10
maligned:
# print struct with more effective memory layout or not, false by default
suggest-new: true
dupl:
# tokens count to trigger issue, 150 by default
threshold: 100
goconst:
# minimal length of string constant, 3 by default
min-len: 3
# minimal occurrences count to trigger, 3 by default
min-occurrences: 3
depguard:
list-type: blacklist
include-go-root: false
packages:
- github.com/davecgh/go-spew/spew
misspell:
# Correct spellings using locale preferences for US or UK.
# Default is to use a neutral variety of English.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
locale: US
ignore-words:
- someword
lll:
# max line length, lines longer will be reported. Default is 120.
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
line-length: 120
# tab width in spaces. Default to 1.
tab-width: 1
unused:
# treat code as a program (not a library) and report unused exported identifiers; default is false.
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
unparam:
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
nakedret:
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
max-func-lines: 30
prealloc:
# XXX: we don't recommend using this linter before doing performance profiling.
# For most programs usage of prealloc will be a premature optimization.
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
# True by default.
simple: true
range-loops: true # Report preallocation suggestions on range loops, true by default
for-loops: false # Report preallocation suggestions on for loops, false by default
gocritic:
# Which checks should be enabled; can't be combined with 'disabled-checks';
# See https://go-critic.github.io/overview#checks-overview
# To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run`
# By default list of stable checks is used.
# enabled-checks:
# - rangeValCopy
# Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint` run to see all tags and checks.
# Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags".
enabled-tags:
- performance
settings: # settings passed to gocritic
captLocal: # must be valid enabled check name
paramsOnly: true
rangeValCopy:
sizeThreshold: 32
linters:
enable-all: true
disable:
- goimports
- stylecheck
- gocritic
- dupl
- gocyclo
- gosec
- lll
- staticcheck
- scopelint
- prealloc
- gofmt
- interfacer # deprecated - "A tool that suggests interfaces is prone to bad suggestions"
#linters:
# enable-all: true
issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
exclude:
- abcdef
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
- path: internal[/\\]config[/\\]
linters:
- gochecknoglobals
# Exclue some linters from running on test files
- path: _test\.go
linters:
- errcheck
# The following are allowed global variable patterns.
# Generally it's ok to have constants or variables that effectively act as constants such as a static logger or flag values.
# The filters below specify the source code pattern that's allowed when declaring a global
# 'source: "flag."' will match 'var destFlag = flag.String("dest", "", "")'
- source: "flag."
linters:
- gochecknoglobals
- source: "View."
linters:
- gochecknoglobals
- source: "tag."
linters:
- gochecknoglobals
- source: "logrus."
linters:
- gochecknoglobals
- source: "stats."
linters:
- gochecknoglobals
# Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via "nolint" comments.
- path: internal/hmac/
text: "weak cryptographic primitive"
linters:
- gosec
# Exclude some staticcheck messages
- linters:
- staticcheck
text: "SA9003:"
# Exclude lll issues for long lines with go:generate
- linters:
- lll
source: "^//go:generate "
# Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all
# excluded by default patterns execute `golangci-lint run --help`.
# Default value for this option is true.
exclude-use-default: false
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0

View File

@ -19,12 +19,12 @@
- A call to the Frontend API `GetUpdates()` gRPC endpoint returns a stream of player messages. This is used to send updates to state storage for the `Assignment`, `Status`, and `Error` Player fields in near-realtime. **It is the responsibility of the game client to disconnect** from the stream when it has gotten the results it was waiting for!
- Moved the rest of the gRPC messages into a shared [`messages.proto` file](api/protobuf-spec/messages.proto).
- Added documentation to Frontend API gRPC calls to the [`frontend.proto` file](api/protobuf-spec/frontend.proto).
- [Issue #41](https://github.com/GoogleCloudPlatform/open-match/issues/41)|[PR #48](https://github.com/GoogleCloudPlatform/open-match/pull/48) There is now a HA Redis install available in `install/yaml/01-redis-failover.yaml`. This would be used as a drop-in replacement for a single-instance Redis configuration in `install/yaml/01-redis.yaml`. The HA configuration requires that you install the [Redis Operator](https://github.com/spotahome/redis-operator) (note: **currently alpha**, use at your own risk) in your Kubernetes cluster.
- [Issue #41](https://github.com/googleforgames/open-match/issues/41)|[PR #48](https://github.com/googleforgames/open-match/pull/48) There is now a HA Redis install available in `install/yaml/01-redis-failover.yaml`. This would be used as a drop-in replacement for a single-instance Redis configuration in `install/yaml/01-redis.yaml`. The HA configuration requires that you install the [Redis Operator](https://github.com/spotahome/redis-operator) (note: **currently alpha**, use at your own risk) in your Kubernetes cluster.
- As part of this change, the kubernetes service name is now `redis` not `redis-sentinel` to denote that it is accessed using a standard Redis client.
- Open Match uses a new feature of the go module [logrus](github.com/sirupsen/logrus) to include filenames and line numbers. If you have an older version in your local build environment, you may need to delete the module and `go get github.com/sirupsen/logrus` again. When building using the provided `cloudbuild.yaml` and `Dockerfile`s this is handled for you.
- The program that was formerly in `examples/frontendclient` has been expanded and has been moved to the `test` directory under (`test/cmd/frontendclient/`)[test/cmd/frontendclient/].
- The client load generator program has been moved from `test/cmd/client` to (`test/cmd/clientloadgen/`)[test/cmd/clientloadgen/] to better reflect what it does.
- [Issue #45](https://github.com/GoogleCloudPlatform/open-match/issues/45) The process for moving the build files (`Dockerfile` and `cloudbuild.yaml`) for each component, example, and test program to their respective directories and out of the repository root has started but won't be completed until a future version.
- [Issue #45](https://github.com/googleforgames/open-match/issues/45) The process for moving the build files (`Dockerfile` and `cloudbuild.yaml`) for each component, example, and test program to their respective directories and out of the repository root has started but won't be completed until a future version.
- Put some basic notes in the [production guide](docs/production.md)
- Added a basic [roadmap](docs/roadmap.md)
@ -54,7 +54,7 @@
- It has become clear from talking to multiple users that the software they write to talk to the Backend API needs a name. 'Backend API Client' is technically correct, but given how many APIs are in Open Match and the overwhelming use of 'Client' to refer to a Game Client in the industry, we're currently calling this a 'Director', as its primary purpose is to 'direct' which profiles are sent to the backend, and 'direct' the resulting MatchObjects to game servers. Further discussion / suggestions are welcome.
- We'll be entering the design stage on longer-running MMFs before the end of the year. We'll get a proposal together and on the github repo as a request for comments, so please keep your eye out for that.
- Match profiles providing multiple MMFs to run isn't planned anymore. Just send multiple copies of the profile with different MMFs specified via the backendapi.
- Redis Sentinel will likely not be supported. Instead, replicated instances and HAProxy may be the HA solution of choice. There's an [outstanding issue to investigate and implement](https://github.com/GoogleCloudPlatform/open-match/issues/41) if it fills our needs, feel free to contribute!
- Redis Sentinel will likely not be supported. Instead, replicated instances and HAProxy may be the HA solution of choice. There's an [outstanding issue to investigate and implement](https://github.com/googleforgames/open-match/issues/41) if it fills our needs, feel free to contribute!
## v0.1.0 (alpha)
Initial release.

View File

@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logic", "Logic\Logic.csproj", "{1EF89BE7-709C-420A-9FDE-6AED2D0FBF2E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data", "Data\Data.csproj", "{0687300A-A514-43FE-924C-29E9203EBD50}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1EF89BE7-709C-420A-9FDE-6AED2D0FBF2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1EF89BE7-709C-420A-9FDE-6AED2D0FBF2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1EF89BE7-709C-420A-9FDE-6AED2D0FBF2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1EF89BE7-709C-420A-9FDE-6AED2D0FBF2E}.Release|Any CPU.Build.0 = Release|Any CPU
{0687300A-A514-43FE-924C-29E9203EBD50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0687300A-A514-43FE-924C-29E9203EBD50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0687300A-A514-43FE-924C-29E9203EBD50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0687300A-A514-43FE-924C-29E9203EBD50}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,32 @@
using System;
namespace Data
{
/// <summary>
/// A generic range-based filter for querying data in ITicketData
/// </summary>
public class Filter
{
public Filter(string key, double min, double max)
{
Key = key;
Min = min;
Max = max;
}
/// <summary>
/// The attribute to query
/// </summary>
public string Key { get; set; }
/// <summary>
/// The minimum value [inclusive] of the range
/// </summary>
public double Min { get; set; }
/// <summary>
/// The maximum value [exclusive] of the range
/// </summary>
public double Max { get; set; }
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Data
{
/// <summary>
/// An interface for creating, deleting, querying, and updating ticket information
/// </summary>
public interface ITicketData
{
/// <summary>
/// Marks the provided ticket ids as ignorable in queries for a period of time
/// </summary>
/// <param name="ticketIds">The ticket ids to be ignored by queries</param>
/// <param name="durationMs"></param>
Task AwaitingAssignmentAsync(IEnumerable<Guid> ticketIds, long durationMs);
/// <summary>
/// Populates the assignment field of the target tickets
/// </summary>
/// <param name="ticketIds">A list of ticket ids to be assigned</param>
/// <param name="assignment">A string containing the assignment of the tickets</param>
/// <returns></returns>
Task AssignTicketsAsync(IEnumerable<Guid> ticketIds, string assignment);
/// <summary>
/// Executes a collection of queries on the ticket data and returns the union of the results
/// </summary>
/// <param name="query">The query to execute</param>
/// <returns>A collection of tickets union of the results</returns>
Task<IEnumerable<Ticket>> QueryTicketsAsync(Query query);
/// <summary>
/// Creates a ticket and adds it to the ticket datastore
/// </summary>
/// <param name="ticket">The ticket to create</param>
/// <remarks>This may create indexes for the data in the ticket to make it queryable</remarks>
Task CreateTicketAsync(Ticket ticket);
/// <summary>
/// Returns a ticket by id
/// </summary>
/// <param name="id">The id of the ticket</param>
/// <returns>The requested ticket if it exists</returns>
Task<Ticket> GetTicketAsync(Guid id);
/// <summary>
/// Deletes a ticket by id
/// </summary>
/// <param name="id">The id of the ticket</param>
/// <remarks>This may delete indexes for the data in the ticket</remarks>
Task DeleteTicketAsync(Guid id);
}
}

View File

@ -0,0 +1,202 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Data
{
public class MemoryData : ITicketData
{
private const string awaitingIndex = "awaitingAssignment";
private const string createdIndex = "created";
protected ConcurrentDictionary<Guid, Ticket> m_Tickets = new ConcurrentDictionary<Guid, Ticket>();
protected ConcurrentDictionary<string, SortedDictionary<Guid, double>> m_Indexes = new ConcurrentDictionary<string, SortedDictionary<Guid, double>>();
public Task AssignTicketsAsync(IEnumerable<Guid> ticketIds, string assignment)
{
foreach (var ticketId in ticketIds)
{
Ticket ticket = m_Tickets[ticketId];
RemoveTicketIndexes(ticket);
ticket.Assignment = assignment;
}
return Task.CompletedTask;
}
public Task AwaitingAssignmentAsync(IEnumerable<Guid> ticketIds, long durationMs)
{
long unixNowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
AddOrCreateIndex(ticketIds, new KeyValuePair<string, double>(awaitingIndex, unixNowMs + durationMs));
return Task.CompletedTask;
}
public Task<IEnumerable<Ticket>> QueryTicketsAsync(Query query)
{
// Validate the query
if (query.Filters == null || query.Filters.Count < 1) throw new ArgumentException("Must specify at least 1 filter");
// Get a copy of the indexes and tickets
Dictionary<string, SortedDictionary<Guid, double>> indexes = new Dictionary<string, SortedDictionary<Guid, double>>();
foreach (var keyValueIndex in m_Indexes)
{
lock (keyValueIndex.Value)
{
indexes.Add(keyValueIndex.Key, new SortedDictionary<Guid, double>(keyValueIndex.Value));
}
}
List<List<Guid>> hits = new List<List<Guid>>();
foreach (var filter in query.Filters)
{
if (indexes.ContainsKey(filter.Key))
{
IEnumerable<Guid> hit = indexes[filter.Key]
.Where(i => i.Value >= filter.Min && i.Value <= filter.Max)
.Select(k => k.Key);
hits.Add(hit.ToList());
}
}
List<Guid> pool = hits.FirstOrDefault();
if (pool == null)
return Task.FromResult<IEnumerable<Ticket>>(new List<Ticket>());
for (int i = 1; i < hits.Count; i++)
{
pool = pool.Intersect(hits[i]).ToList();
}
// Built-in ignore index
if (indexes.ContainsKey(awaitingIndex))
{
long unixNowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
IEnumerable<Guid> ignore = indexes[awaitingIndex].Where(i => i.Value >= unixNowMs).Select(k => k.Key);
pool = pool.Except(ignore).ToList(); // TODO: the list is ordered and except doesn't take advantage of that
}
List<Ticket> ticketList = new List<Ticket>();
foreach (var guid in pool)
{
ticketList.Add(m_Tickets[guid]);
}
return Task.FromResult<IEnumerable<Ticket>>(ticketList);
}
public Task CreateTicketAsync(Ticket ticket)
{
// Validate the ticket
if (ticket == null) throw new ArgumentNullException(nameof(ticket));
if (ticket.Attributes == null) throw new ArgumentNullException(paramName: "ticketAttributes");
if (ticket.Attributes.Count == 0) throw new ArgumentException("There must be at least 1 attribute to index", paramName: "ticketAttributes");
Ticket newTicket = new Ticket()
{
Id = ticket.Id,
Attributes = new Dictionary<string, double>(ticket.Attributes),
Properties = ticket.Properties,
Created = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
Assignment = string.IsNullOrEmpty(ticket.Assignment) ? string.Empty : ticket.Assignment
};
AddTicketIndexes(newTicket);
m_Tickets.TryAdd(newTicket.Id, newTicket);
return Task.CompletedTask;
}
public Task<Ticket> GetTicketAsync(Guid id)
{
if (m_Tickets.TryGetValue(id, out Ticket ticket))
{
return Task.FromResult(ticket);
}
throw new Exception("Not Found");
}
public Task DeleteTicketAsync(Guid id)
{
Ticket ticket = m_Tickets[id];
RemoveTicketIndexes(ticket);
m_Tickets.TryRemove(id, out ticket);
return Task.CompletedTask;
}
private void AddTicketIndexes(Ticket ticket)
{
foreach (var attribute in ticket.Attributes)
{
AddOrCreateIndex(ticket.Id, attribute);
}
// Built-in indexes
AddOrCreateIndex(ticket.Id, new KeyValuePair<string, double>(createdIndex, ticket.Created));
}
/// <summary>
/// The bulk version for reducing locks when updating lots of records at once with the same attribute
/// </summary>
private void AddOrCreateIndex(IEnumerable<Guid> ids, KeyValuePair<string, double> attribute)
{
if (!m_Indexes.ContainsKey(attribute.Key))
{
m_Indexes.TryAdd(attribute.Key, new SortedDictionary<Guid, double>());
}
lock (m_Indexes[attribute.Key])
{
foreach (var id in ids)
{
m_Indexes[attribute.Key].Add(id, attribute.Value);
}
}
}
private void AddOrCreateIndex(Guid id, KeyValuePair<string, double> attribute)
{
if (m_Indexes.ContainsKey(attribute.Key))
{
lock (m_Indexes[attribute.Key])
{
m_Indexes[attribute.Key].Add(id, attribute.Value);
}
}
else
{
m_Indexes.TryAdd(attribute.Key, new SortedDictionary<Guid, double>() { { id, attribute.Value } });
}
}
private void RemoveTicketIndexes(Ticket ticket)
{
foreach (var attribute in ticket.Attributes)
{
DeleteIndex(ticket.Id, attribute.Key);
}
}
private void DeleteIndex(Guid id, string attributeKey)
{
if (m_Indexes.ContainsKey(attributeKey))
{
lock (m_Indexes[attributeKey])
{
m_Indexes[attributeKey].Remove(id);
}
}
}
public async Task CreateTicketsAsync(IEnumerable<Ticket> tickets)
{
foreach (var ticket in tickets)
{
await CreateTicketAsync(ticket);
}
}
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
namespace Data
{
/// <summary>
/// Captures a searching behavior for ITicketData
/// </summary>
public class Query
{
public Query()
{
}
public Query(List<Filter> filters)
{
Filters = filters;
}
/// <summary>
/// A list of hard filters to be applied to the provided searchable attributes
/// </summary>
public List<Filter> Filters { get; set; }
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace Data
{
/// <summary>
/// Intended to be used as a data model abstraction for handling groups of players organized into indexable tickets
/// </summary>
public class Ticket
{
/// <summary>
/// The identifier of the ticket tracked by clients
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// A contract for allowing a backend to provide assignment information
/// </summary>
public string Assignment { get; set; }
/// <summary>
/// Range indexes
/// </summary>
public IDictionary<string, double> Attributes { get; set; }
/// <summary>
/// The milliseconds in unix utc representing when this ticket was created
/// </summary>
public long Created {get;set;}
/// <summary>
/// Custom data provided by the ticket creator
/// </summary>
public JObject Properties { get; set; }
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Net.Http;
using Data;
using Logic.InternalContracts;
using Microsoft.Extensions.Logging;
namespace Logic
{
public class FunctionClientResolver
{
readonly HttpClient m_HttpClient;
ILoggerFactory m_LoggerFactory;
public FunctionClientResolver(IServiceProvider serviceProvider, HttpClient httpClient, ITicketData ticketData, ILoggerFactory loggerFactory)
{
m_HttpClient = httpClient;
m_LoggerFactory = loggerFactory;
}
public IFunctionClient GetFunctionClientByTarget(TargetFunction target)
{
switch (target.Kind)
{
case FunctionKind.Rest:
return new FunctionRestClient(m_HttpClient, target, m_LoggerFactory.CreateLogger<FunctionRestClient>());
default:
throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Logic.InternalContracts;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Logic
{
public class FunctionRestClient : IFunctionClient
{
HttpClient m_Client;
string m_Address;
ILogger<FunctionRestClient> m_Log;
public FunctionRestClient(HttpClient client, TargetFunction targetFunction, ILogger<FunctionRestClient> log)
{
m_Log = log;
m_Client = client;
IPHostEntry dns = Dns.GetHostEntry(targetFunction.Name);
m_Address = dns.AddressList[0].ToString();
}
public async Task<IEnumerable<Match>> RunAsync(MatchSpec spec, CancellationToken cancellationToken)
{
FunctionRestParams context = new FunctionRestParams()
{
Pools = new List<Pool>(),
Config = spec.Config
};
foreach (var specPool in spec.Pools)
{
context.Pools.Add(new Pool() { Name = specPool.Key, Filters = specPool.Value });
}
string json = JsonConvert.SerializeObject(context);
string url = "http://" + m_Address + ":8080" + "/api/function";
m_Log.LogDebug("Calling {Url} with body {Body}", url, json);
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = new StringContent(json);
request.Content.Headers.Clear();
request.Content.Headers.Add("Content-Type", "application/json");
HttpResponseMessage message = await m_Client.SendAsync(request, cancellationToken);
if (!message.IsSuccessStatusCode)
{
m_Log.LogWarning("{StatusCode} received from function {Url}. {Reason}", message.StatusCode, m_Address, message.ReasonPhrase);
}
string body = await message.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<List<Match>>(body);
}
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Logic.InternalContracts;
namespace Logic
{
public interface IEvaluator
{
Task<List<Match>> Evaluate(List<Match> Matches);
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Logic.InternalContracts;
namespace Logic
{
public interface IFunctionClient
{
Task<IEnumerable<Match>> RunAsync(MatchSpec config, CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Logic.InternalContracts;
namespace Logic
{
public interface IMatchmakingBackend
{
Task<List<Match>> GetMatchesAsync(List<MatchSpec> matchSpecs, CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace Logic.InternalContracts
{
public class FunctionRestParams
{
public JObject Config { get; set; }
public List<Pool> Pools { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using Data;
using Newtonsoft.Json.Linq;
namespace Logic.InternalContracts
{
public class Match
{
public Guid Id { get; set; }
public List<Ticket> Tickets { get; set; }
public JObject Properties { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using Data;
using Newtonsoft.Json.Linq;
namespace Logic.InternalContracts
{
public class MatchSpec
{
public TargetFunction Target { get; set; }
public JObject Config { get; set; }
public IDictionary<string, List<Filter>> Pools { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using Data;
namespace Logic.InternalContracts
{
/// <summary>
/// A generalized matchmaking "hard" filtering description. Consists of sets of filters
/// </summary>
public class Pool
{
/// <summary>
/// A friendly name identifier for the pool
/// </summary>
public string Name { get; set; }
/// <summary>
/// The collection of generic filters for performing query logic
/// </summary>
public List<Filter> Filters { get; set; }
}
}

View File

@ -0,0 +1,21 @@
using System;
namespace Logic.InternalContracts
{
public class TargetFunction
{
public string Name { get; set; }
public string Version { get; set; }
public FunctionKind Kind { get; set; }
}
public enum FunctionKind
{
None,
Rest,
Grpc,
Memory
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Data\Data.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="2.2.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data;
using Logic.InternalContracts;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
namespace Logic
{
public class MatchmakingBackend : IMatchmakingBackend
{
ITicketData m_TicketData;
SynchronizationContext m_SyncContext;
ILogger<MatchmakingBackend> m_Logger;
FunctionClientResolver m_FunctionClientResolver;
public MatchmakingBackend(ITicketData ticketData, ILogger<MatchmakingBackend> logger, FunctionClientResolver resolver, SynchronizationContext syncContext)
{
m_TicketData = ticketData;
m_Logger = logger;
m_FunctionClientResolver = resolver;
m_SyncContext = syncContext;
}
public async Task<List<Match>> GetMatchesAsync(List<MatchSpec> matchSpecs, CancellationToken cancellationToken)
{
// Generate a cancellation time for all the functions. TODO: Make the global timeout configurable
CancellationToken token = AddTimeCancellationToken(cancellationToken, 60000);
Guid contextRegistrationId = await m_SyncContext.AcquireContext();
// Execute functions in parallel
Stopwatch watch = Stopwatch.StartNew();
List<Task<IEnumerable<Match>>> tasks = new List<Task<IEnumerable<Match>>>();
foreach (var matchSpec in matchSpecs)
{
IFunctionClient client = m_FunctionClientResolver.GetFunctionClientByTarget(matchSpec.Target);
m_Logger.LogInformation("Running target {Target} as {Kind}", matchSpec.Target.Name, matchSpec.Target.Kind);
tasks.Add(Task.Run(() => client.RunAsync(matchSpec, token)));
}
// Wait for all the Matches to come back in
List<Match> Matches = (await Task.WhenAll(tasks)).SelectMany(r => r.AsEnumerable()).ToList();
m_Logger.LogInformation("Function run time {ElapsedMs}ms. Submitting {MatchCount} Matches for evaluation.", watch.ElapsedMilliseconds, Matches.Count);
watch.Restart();
// Send the Matches to the evaluator, which will automatically synchronize the Matches
List<Match> goodMatches = await m_SyncContext.EvaluateAsync(contextRegistrationId, Matches);
m_Logger.LogDebug("Evaluator waiting time {ElapsedMs}ms", watch.ElapsedMilliseconds);
// Tell the data api so it can start de-indexing those players
List<Match> matches = new List<Match>();
foreach (var Match in goodMatches)
{
matches.Add(new Match()
{
Properties = JObject.FromObject(Match.Properties),
Tickets = Match.Tickets,
// MatchSpec = null TODO: Maybe re-associate the original matchSpec
});
}
return matches;
}
static CancellationToken AddTimeCancellationToken(CancellationToken token, int ms)
{
CancellationTokenSource cts = new CancellationTokenSource(ms);
return CancellationTokenSource.CreateLinkedTokenSource(token, cts.Token).Token;
}
}
}

View File

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Logic.InternalContracts;
using Microsoft.Extensions.Logging;
namespace Logic
{
/// <summary>
/// A Match de-collider takes non-colliding Matches in descending score order
/// </summary>
public class ScoreEvaluator : IEvaluator
{
ILogger<ScoreEvaluator> m_Log { get; }
public ScoreEvaluator(ILogger<ScoreEvaluator> log)
{
m_Log = log;
}
public Task<List<Match>> Evaluate(List<Match> Matches)
{
m_Log.LogDebug("{MatchCount} Matches to be evaluated", Matches.Count);
// Sort the Matches by score
Matches = Matches.OrderByDescending(p => p.Properties["score"]).ToList();
List<Match> goodMatches = new List<Match>();
HashSet<Guid> ticketsPresent = new HashSet<Guid>();
foreach (var nextMatch in Matches)
{
// Optimize by converting the prop tickets to a hashset
var propTickets = nextMatch.Tickets.Select(t => t.Id).ToHashSet();
// Check if any of the tickets in the Match are already spoken for
if (ticketsPresent.Overlaps(propTickets))
continue;
// If not, the Match is a good match and mark the tickets as spoken for
goodMatches.Add(nextMatch);
foreach (var ticketId in propTickets)
{
ticketsPresent.Add(ticketId);
}
}
m_Log.LogDebug("{MatchesApproved} Matches approved in evaluation. {TicketsApproved} tickets approved", goodMatches.Count, ticketsPresent.Count);
return Task.FromResult(goodMatches);
}
}
}

View File

@ -0,0 +1,252 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data;
using Logic.InternalContracts;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Logic
{
/// <summary>
/// A threadsafe way to run an evaluator. Intended to be used as a singleton, or shared context behind a service
/// </summary>
public class SynchronizationContext
{
int m_MinRunMs;
int m_MaxRunMs;
ITicketData m_TicketData;
IEvaluator m_Evaluator;
ILogger<SynchronizationContext> m_Logger;
ConcurrentDictionary<Guid, List<Match>> m_ContextMatches = new ConcurrentDictionary<Guid, List<Match>>();
ConcurrentDictionary<Guid, List<Guid>> m_ContextResults = new ConcurrentDictionary<Guid, List<Guid>>();
ConcurrentDictionary<Guid, bool> m_ExistingContexts = new ConcurrentDictionary<Guid, bool>();
ManualResetEvent m_NewContextsAvailable = new ManualResetEvent(false);
ManualResetEvent m_ResultsAvailable = new ManualResetEvent(false);
bool m_AcceptingMatches = false;
Timer m_Timer;
Stopwatch m_Watch = new Stopwatch();
Task m_EvalTask = null;
SyncState m_State = SyncState.NotRunning;
object startLock = new object();
enum SyncState
{
NotRunning,
AcceptingContexts,
AcceptingMatches,
Evaluating
}
public SynchronizationContext(ILogger<SynchronizationContext> logger, ITicketData ticketData, IEvaluator evaluator, IOptions<SynchronizationOptions> options)
{
m_Logger = logger;
m_MinRunMs = options.Value.MinWindowSizeMs;
m_MaxRunMs = options.Value.MaxWindowSizeMs;
m_Evaluator = evaluator;
m_TicketData = ticketData;
// TODO: Make the loop event schedulable instead of loop driven
m_Timer = new Timer(
UpdateState,
new AutoResetEvent(true),
options.Value.StateMachineUpdateMs,
options.Value.StateMachineUpdateMs
);
}
/// <summary>
/// Thread safe way to acquire a contextId and register for the evaluator
/// </summary>
/// <returns>A contextId</returns>
public async Task<Guid> AcquireContext()
{
Stopwatch watch = Stopwatch.StartNew();
Guid contextId = await WaitRegisterContextAsync();
m_Logger.LogDebug("{ElapsedMs}ms to acquire context", watch.ElapsedMilliseconds);
return contextId;
}
/// <summary>
/// Thread safe way to let the evaluator de-collide the passed in Matches with other contexts
/// </summary>
/// <param name="contextId">The id of this context</param>
/// <param name="Matches">Matches to de-collide</param>
/// <returns>A list of de-collided Matches</returns>
/// <exception cref="Exception"></exception>
public async Task<List<Match>> EvaluateAsync(Guid contextId, List<Match> Matches)
{
Stopwatch watch = Stopwatch.StartNew();
// Try to register the Matches with the machine
if (TryRegisterMatches(contextId, Matches))
{
m_Logger.LogDebug("{ElapsedMs}ms to register Matches", watch.ElapsedMilliseconds);
watch.Restart();
// Wait for the machine to run and return my results
List<Guid> good = await WaitResultsAsync(contextId);
m_Logger.LogDebug("{ElapsedMs}ms evaluation results available", watch.ElapsedMilliseconds);
List<Match> goodMatches = new List<Match>();
foreach (var prop in Matches)
{
if (good.Contains(prop.Id))
{
goodMatches.Add(prop);
}
}
return goodMatches;
}
throw new Exception("Match registration failed");
}
/// <summary>
/// Attempt to wait for context acquisition to become available and register for one. If the evaluator
/// is not running, it will set the state machine into a runnable state
/// </summary>
/// <returns></returns>
private Task<Guid> WaitRegisterContextAsync()
{
// If the machine isn't started, try to start it
if (m_State == SyncState.NotRunning)
{
lock(startLock)
{
// Make sure this call got the lock in time, otherwise bail
if (m_State == SyncState.NotRunning)
{
// The machine isn't running so clear the current results, any registrations, and any Matches
m_ContextResults.Clear();
m_ExistingContexts.Clear();
m_ContextMatches.Clear();
// Allow new contexts to register, allow new Matches, and disallow results reading
m_State = SyncState.AcceptingContexts;
m_NewContextsAvailable.Set();
m_ResultsAvailable.Reset();
m_AcceptingMatches = true;
m_Watch = Stopwatch.StartNew();
}
}
}
m_NewContextsAvailable.WaitOne(5000); // TODO: Make this wait timeout automated
Guid newId = Guid.NewGuid();
m_ExistingContexts.TryAdd(newId, false);
return Task.FromResult(newId);
}
private Task<List<Guid>> WaitResultsAsync(Guid contextId)
{
m_ResultsAvailable.WaitOne(5000); // TODO: Make this wait timeout automated
return Task.FromResult(m_ContextResults[contextId]);
}
private bool TryRegisterMatches(Guid contextId, List<Match> Matches)
{
if (!m_ExistingContexts.ContainsKey(contextId)) return false;
if (!m_AcceptingMatches) return false;
m_ExistingContexts[contextId] = true;
m_ContextResults.TryAdd(contextId, new List<Guid>());
return m_ContextMatches.TryAdd(contextId, Matches);
}
private void UpdateState(object state)
{
switch (m_State)
{
case SyncState.NotRunning:
break;
case SyncState.AcceptingContexts:
if (m_Watch.ElapsedMilliseconds > m_MinRunMs)
{
m_Logger.LogDebug("Min window passed at {ElapsedMs}ms", m_Watch.ElapsedMilliseconds);
m_State = SyncState.AcceptingMatches;
m_NewContextsAvailable.Reset();
UpdateState(state); // Just go ahead and check the accepting state
}
break;
case SyncState.AcceptingMatches:
bool maxWindowExceeded = m_Watch.ElapsedMilliseconds > m_MaxRunMs;
bool allIn = m_ExistingContexts.Values.All(b => b);
if (m_Watch.ElapsedMilliseconds > m_MaxRunMs || m_ExistingContexts.Values.All(b => b))
{
if (maxWindowExceeded) m_Logger.LogDebug("Max window exceeded. Moving to eval at {ElapsedMs}ms", m_Watch.ElapsedMilliseconds);
if (allIn) m_Logger.LogDebug("All contexts reported in. Moving to eval at {ElapsedMs}ms", m_Watch.ElapsedMilliseconds);
m_State = SyncState.Evaluating;
m_AcceptingMatches = false;
m_EvalTask = Task.Run(async () =>
{
// Run the evaluator in parallel to this state system
await RunEvaluation(m_Evaluator);
// Once done, clear the other threads to read from the results. Set the machine back to doing nothing
m_State = SyncState.NotRunning;
m_ResultsAvailable.Set();
m_Logger.LogDebug("Evaluation completed at {ElapsedMs}ms", m_Watch.ElapsedMilliseconds);
m_Watch.Reset();
});
}
break;
case SyncState.Evaluating:
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private async Task RunEvaluation(IEvaluator evaluator)
{
// Create a reverse map of context to Match (to rebuild the results at the end)
Dictionary<Guid, Guid> MatchIdToContextId = new Dictionary<Guid, Guid>();
List<Match> allMatches = new List<Match>();
foreach (var contextMatch in m_ContextMatches)
{
foreach (var Match in contextMatch.Value)
{
allMatches.Add(Match);
MatchIdToContextId.Add(Match.Id, contextMatch.Key);
}
}
// Run the evaluator
List<Match> matches = await evaluator.Evaluate(allMatches);
// Flag the selected tickets as un-queryable for a set period of time. // TODO: Make configurable
List<Guid> ticketsTaken = matches.SelectMany(m => m.Tickets.Select(t => t.Id)).ToList();
await m_TicketData.AwaitingAssignmentAsync(ticketsTaken, 60000);
// Put the match results into the proper results context. TODO: Failure handled good enough by unqueryable timeout?
foreach (var match in matches)
{
Guid contextId = MatchIdToContextId[match.Id];
m_ContextResults[contextId].Add(match.Id);
}
}
}
}

View File

@ -0,0 +1,15 @@
using System;
namespace Logic
{
public class SynchronizationOptions
{
public const string SectionName = "SynchronizationOptions";
public int MinWindowSizeMs { get; set; }
public int MaxWindowSizeMs { get; set; }
public int StateMachineUpdateMs { get; set; }
}
}

View File

@ -15,7 +15,7 @@
FROM golang:latest
ENV GO111MODULE=on
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match
WORKDIR /go/src/open-match.dev/open-match
COPY . .
RUN go mod download

View File

@ -31,7 +31,7 @@ RUN sudo apt-get install -y -qq docker-ce docker-ce-cli containerd.io
RUN export CLOUD_SDK_REPO="cloud-sdk-stretch" && \
echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && \
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \
apt-get update -y && apt-get install google-cloud-sdk -y -qq
apt-get update -y && apt-get install google-cloud-sdk google-cloud-sdk-app-engine-go -y -qq
# Install Golang
# https://github.com/docker-library/golang/blob/fd272b2b72db82a0bd516ce3d09bba624651516c/1.12/stretch/Dockerfile

700
Makefile

File diff suppressed because it is too large Load Diff

172
README.md
View File

@ -1,144 +1,47 @@
![Open Match](site/static/images/logo-with-name.png)
[![GoDoc](https://godoc.org/github.com/GoogleCloudPlatform/open-match?status.svg)](https://godoc.org/github.com/GoogleCloudPlatform/open-match)
[![Go Report Card](https://goreportcard.com/badge/github.com/GoogleCloudPlatform/open-match)](https://goreportcard.com/report/github.com/GoogleCloudPlatform/open-match)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/GoogleCloudPlatform/open-match/blob/master/LICENSE)
[![GoDoc](https://godoc.org/open-match.dev/open-match?status.svg)](https://godoc.org/open-match.dev/open-match)
[![Go Report Card](https://goreportcard.com/badge/open-match.dev/open-match)](https://goreportcard.com/report/open-match.dev/open-match)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/googleforgames/open-match/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/release-pre/googleforgames/open-match.svg)](https://github.com/googleforgames/open-match/releases)
[![Follow on Twitter](https://img.shields.io/twitter/follow/Open_Match.svg?style=social&logo=twitter)](https://twitter.com/intent/follow?screen_name=Open_Match)
Open Match is an open source game matchmaking framework designed to allow game creators to build matchmakers of any size easily and with as much possibility for sharing and code re-use as possible. Its designed to be flexible, extensible, and scalable.
Open Match is a flexible, extensible, and scalable game matchmaking framework.
Matchmaking begins when a player tells the game that they want to play. Every player has a set of attributes like skill, location, playtime, win-lose ratio, etc which may factor in how they are paired with other players. Typically, there's a trade off between the quality of the match vs the time to wait. Since Open Match is designed to scale with the player population, it should be possible to still have high quality matches while having high player count.
Open Match replaces the complex plumbing parts of matchmaking and allows you to
focus on the details of making a good match based on your game.
Under the covers matchmaking approaches touch on significant areas of computer science including graph theory and massively concurrent processing. Open Match is an effort to provide a foundation upon which these difficult problems can be addressed by the wider game development community. As Josh Menke &mdash; famous for working on matchmaking for many popular triple-A franchises &mdash; put it:
## Disclaimer
["Matchmaking, a lot of it actually really is just really good engineering. There's a lot of really hard networking and plumbing problems that need to be solved, depending on the size of your audience."](https://youtu.be/-pglxege-gU?t=830)
This software is currently alpha, and subject to change. Although Open Match has
already been used to run [production workloads within Google](https://cloud.google.com/blog/topics/inside-google-cloud/no-tricks-just-treats-globally-scaling-the-halloween-multiplayer-doodle-with-open-match-on-google-cloud).
This project attempts to solve the networking and plumbing problems, so game developers can focus on the logic to match players into great games.
## Usage
## Running Open Match
Open Match framework is a collection of servers that run within Kubernetes (the [puppet master](https://en.wikipedia.org/wiki/Puppet_Master_(gaming)) for your server cluster.)
## Deploy to Kubernetes
If you have an [existing Kubernetes cluster](https://cloud.google.com/kubernetes-engine/docs/how-to/creating-a-cluster) you can run these commands to install Open Match.
```bash
# Grant yourself cluster-admin permissions so that you can deploy service accounts.
kubectl create clusterrolebinding myname-cluster-admin-binding --clusterrole=cluster-admin --user=$(YOUR_KUBERNETES_USER_NAME)
# Place all Open Match components in their own namespace.
kubectl create namespace open-match
# Install Open Match and monitoring services.
kubectl apply -f https://storage.googleapis.com/open-match-chart/install/yaml/master-latest/install.yaml --namespace open-match
# Install the example MMF and Evaluator.
kubectl apply -f https://storage.googleapis.com/open-match-chart/install/yaml/master-latest/install-example.yaml --namespace open-match
```
To delete Open Match
```bash
# Delete the open-match namespace that holds all the Open Match configuration.
kubectl delete namespace open-match
```
## Development
Open Match can be deployed locally or in the cloud for development. Below are the steps to build, push, and deploy the binaries to Kubernetes.
### Deploy to Minikube (Locally)
[Minikube](https://kubernetes.io/docs/setup/minikube/) is Kubernetes in a VM. It's mainly used for development.
```bash
# Create a Minikube Cluster and install Helm
make create-mini-cluster push-helm
# Deploy Open Match with example functions
make REGISTRY=gcr.io/open-match-public-images TAG=latest install-chart install-example-chart
```
### Deploy to Google Cloud Platform (Cloud)
Create a GCP project via [Google Cloud Console](https://console.cloud.google.com/). Billing must be enabled but if you're a new customer you can get some [free credits](https://cloud.google.com/free/). When you create a project you'll need to set a Project ID, if you forget it you can see it here, https://console.cloud.google.com/iam-admin/settings/project.
Now install [Google Cloud SDK](https://cloud.google.com/sdk/) which is the command line tool to work against your project. The following commands log you into your GCP Project.
```bash
# Login to your Google Account for GCP.
gcloud auth login
gcloud config set project $YOUR_GCP_PROJECT_ID
# Enable GCP services
gcloud services enable containerregistry.googleapis.com
gcloud services enable container.googleapis.com
# Test that everything is good, this command should work.
gcloud compute zones list
```
Please follow the instructions to [Setup Local Open Match Repository](#local-repository-setup). Once everything is setup you can deploy Open Match by creating a cluster in Google Kubernetes Engine (GKE).
```bash
# Create a GKE Cluster and install Helm
make create-gke-cluster push-helm
# Deploy Open Match with example functions
make REGISTRY=gcr.io/open-match-build TAG=0.4.0-e98e1b6 install-chart install-example-chart
```
To generate matches using a test client, run the following command:
```bash
make REGISTRY=gcr.io/open-match-build TAG=0.4.0-e98e1b6 run-backendclient
```
Once deployed you can view the jobs in [Cloud Console](https://console.cloud.google.com/kubernetes/workload).
### Local Repository Setup
Here are the instructions to set up a local repository for Open Match.
```bash
# Install Open Match Toolchain Dependencies (for Debian, other OSes including Mac OS X have similar dependencies)
sudo apt-get update; sudo apt-get install -y -q python3 python3-virtualenv virtualenv make google-cloud-sdk git unzip tar
# Setup your repository like Go workspace, https://golang.org/doc/code.html#Workspaces
# This requirement will go away soon.
mkdir -p $HOME/workspace/src/github.com/GoogleCloudPlatform/
cd $HOME/workspace/src/github.com/GoogleCloudPlatform/
export GOPATH=$HOME/workspace
export GO111MODULE=on
git clone https://github.com/GoogleCloudPlatform/open-match.git
cd open-match
```
### Compiling From Source
The easiest way to build Open Match is to use the [Makefile](Makefile). Please follow the instructions to [Setup Local Open Match Repository](#local-repository-setup).
[Docker](https://docs.docker.com/install/) and [Go 1.12+](https://golang.org/dl/) is also required.
To build all the artifacts of Open Match you can simply run the following commands.
```bash
# Downloads all the tools needed to build Open Match
make install-toolchain
# Generates protocol buffer code files
make all-protos
# Builds all the binaries
make all
# Builds all the images.
make build-images
```
Once build you can use a command like `docker images` to see all the images that were build.
Before creating a pull request you can run `make local-cloud-build` to simulate a Cloud Build run to check for regressions.
The directory structure is a typical Go structure so if you do the following you should be able to work on this project within your IDE.
Lastly, this project uses go modules so you'll want to set `export GO111MODULE=on` in your `~/.bashrc`.
The [Build Queue](https://console.cloud.google.com/cloud-build/builds?project=open-match-build) runs against all PRs, requires membership to [open-match-discuss@googlegroups.com](https://groups.google.com/forum/#!forum/open-match-discuss).
Documentation can be found on the [Open Match website](https://open-match.dev/site/docs/).
## Support
* [Slack Channel](https://open-match.slack.com/) ([Signup](https://join.slack.com/t/open-match/shared_invite/enQtNDM1NjcxNTY4MTgzLWQzMzE1MGY5YmYyYWY3ZjE2MjNjZTdmYmQ1ZTQzMmNiNGViYmQyN2M4ZmVkMDY2YzZlOTUwMTYwMzI1Y2I2MjU))
* [File an Issue](https://github.com/GoogleCloudPlatform/open-match/issues/new)
* [File an Issue](https://github.com/googleforgames/open-match/issues/new)
* [Mailing list](https://groups.google.com/forum/#!forum/open-match-discuss)
* [Managed Service Survey](https://goo.gl/forms/cbrFTNCmy9rItSv72)
## Code of Conduct
Participation in this project comes under the [Contributor Covenant Code of Conduct](code-of-conduct.md)
## Documentation
Here are some useful links to additional documentation:
* [Get Started](https://open-match.dev/site/docs/installation/)
* [Development Guide](docs/development.md)
* [Future Roadmap](docs/roadmap.md)
* [Open Match Concepts](docs/concepts.md)
* [Open Match Integrations](docs/integrations.md)
* [References](docs/references.md)
For more information on the technical underpinnings of Open Match you can refer to the [docs/](docs/) directory.
## Contributing
@ -146,17 +49,8 @@ Please read the [contributing](CONTRIBUTING.md) guide for directions on submitti
See the [Development guide](docs/development.md) for documentation for development and building Open Match from source.
The [Release Process](docs/governance/release_process.md) documentation displays the project's upcoming release calendar and release process.
Open Match is in active development - we would love your help in shaping its future!
## Documentation
## License
For more information on the technical underpinnings of Open Match you can refer to the [docs/](docs/) directory.
## Code of Conduct
Participation in this project comes under the [Contributor Covenant Code of Conduct](code-of-conduct.md)
## Disclaimer
This software is currently alpha, and subject to change. Although Open Match has already been used to run [production workloads within Google](https://cloud.google.com/blog/topics/inside-google-cloud/no-tricks-just-treats-globally-scaling-the-halloween-multiplayer-doodle-with-open-match-on-google-cloud), but it's still early days on the way to our final goal. There's plenty left to write and we welcome contributions. **We strongly encourage you to engage with the community through the [Slack or Mailing lists](#support) if you're considering using Open Match in production before the 1.0 release, as the documentation is likely to lag behind the latest version a bit while we focus on getting out of alpha/beta as soon as possible.**
Apache 2.0

View File

@ -1,12 +1,9 @@
# Open Match APIs
# Open Match API
This directory contains the API specification files for Open Match. API documenation will be produced in a future version, although the protobuf files offer a concise description of the API calls available, along with arguments and return messages.
Open Match API is exposed via [gRPC](https://grpc.io/) and HTTP REST with [Swagger](https://swagger.io/tools/swagger-codegen/).
* [Protobuf .proto files for all APIs](./protobuf-spec/)
gRPC has first-class support for [many languages](https://grpc.io/docs/) and provides the most performance. It is a RPC protocol built on top of HTTP/2 and provides TLS for secure transport.
References:
For HTTP/HTTPS Open Match uses a gRPC proxy to serve the API. Since HTTP does not provide a structure for request/responses we use Swagger to provide a schema. You can view the Swagger docs for each service in this directory's `*.swagger.json` files. In addition each server will host it's swagger doc via `GET /swagger.json` if you want to dynamically load them at runtime.
* [gRPC](https://grpc.io/)
* [Language Guide (proto3)](https://developers.google.com/protocol-buffers/docs/proto3)
If you want to regenerate the golang gRPC modules (for local Open Match core component development, for example), the `protoc_go.sh` file in this directory may be of use to you!
Lastly, Open Match supports insecure and TLS mode for serving the API. It's strongly preferred to use TLS mode in production but insecure mode can be used for test and local development. To help with certificates management see `tools/certgen` to create self-signed certificates.

127
api/backend.proto Normal file
View File

@ -0,0 +1,127 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package api;
option go_package = "internal/pb";
import "api/messages.proto";
import "google/api/annotations.proto";
import "protoc-gen-swagger/options/annotations.proto";
option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
info: {
title: "Backend"
version: "1.0"
contact: {
name: "Open Match"
url: "https://open-match.dev"
email: "open-match-discuss@googlegroups.com"
}
license: {
name: "Apache 2.0 License"
url: "https://github.com/googleforgames/open-match/blob/master/LICENSE"
}
}
external_docs: {
url: "https://open-match.dev/site/docs/"
description: "Open Match Documentation"
}
schemes: HTTP
schemes: HTTPS
consumes: "application/json"
produces: "application/json"
responses: {
key: "404"
value: {
description: "Returned when the resource does not exist."
schema: { json_schema: { type: STRING } }
}
}
// TODO Add annotations for security_defintiions.
// See
// https://github.com/grpc-ecosystem/grpc-gateway/blob/master/examples/proto/examplepb/a_bit_of_everything.proto
};
// Configuration for a GRPC Match Function
message GrpcFunctionConfig {
string host = 1;
int32 port = 2;
}
// Configuration for a REST Match Function.
message RestFunctionConfig {
string host = 1;
int32 port = 2;
}
// Configuration for the Match Function to be triggered by Open Match to
// generate proposals.
message FunctionConfig {
// A developer-chosen human-readable name for this Match Function.
string name = 1;
// Properties for the type of this function.
oneof type {
GrpcFunctionConfig grpc = 10001;
RestFunctionConfig rest = 10002;
}
}
message FetchMatchesRequest {
// Configuration of the MatchFunction to be executed for the given list of MatchProfiles
FunctionConfig config = 1;
// MatchProfiles for which this MatchFunction should be executed.
repeated MatchProfile profile = 2;
}
message FetchMatchesResponse {
// Result Match for the requested MatchProfile.
Match match = 1;
}
message AssignTicketsRequest {
// List of Ticket IDs for which the Assignment is to be made.
repeated string ticket_id = 1;
// Assignment to be associated with the Ticket IDs.
Assignment assignment = 2;
}
message AssignTicketsResponse {}
// The service implementing the Backent API that is called to generate matches
// and make assignments for Tickets.
service Backend {
// FetchMatch triggers execution of the specfied MatchFunction for each of the
// specified MatchProfiles. Each MatchFunction execution returns a set of
// proposals which are then evaluated to generate results. FetchMatch method
// streams these results back to the caller.
rpc FetchMatches(FetchMatchesRequest) returns (stream FetchMatchesResponse) {
option (google.api.http) = {
post: "/v1/backend/matches:fetch"
body: "*"
};
}
// AssignTickets sets the specified Assignment on the Tickets for the Ticket
// IDs passed.
rpc AssignTickets(AssignTicketsRequest) returns (AssignTicketsResponse) {
option (google.api.http) = {
post: "/v1/backend/tickets:assign"
body: "*"
};
}
}

459
api/backend.swagger.json Normal file
View File

@ -0,0 +1,459 @@
{
"swagger": "2.0",
"info": {
"title": "Backend",
"version": "1.0",
"contact": {
"name": "Open Match",
"url": "https://open-match.dev",
"email": "open-match-discuss@googlegroups.com"
},
"license": {
"name": "Apache 2.0 License",
"url": "https://github.com/googleforgames/open-match/blob/master/LICENSE"
}
},
"schemes": [
"http",
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/backend/matches:fetch": {
"post": {
"summary": "FetchMatch triggers execution of the specfied MatchFunction for each of the\nspecified MatchProfiles. Each MatchFunction execution returns a set of\nproposals which are then evaluated to generate results. FetchMatch method\nstreams these results back to the caller.",
"operationId": "FetchMatches",
"responses": {
"200": {
"description": "A successful response.(streaming responses)",
"schema": {
"$ref": "#/x-stream-definitions/apiFetchMatchesResponse"
}
},
"404": {
"description": "Returned when the resource does not exist.",
"schema": {
"format": "string"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiFetchMatchesRequest"
}
}
],
"tags": [
"Backend"
]
}
},
"/v1/backend/tickets:assign": {
"post": {
"summary": "AssignTickets sets the specified Assignment on the Tickets for the Ticket\nIDs passed.",
"operationId": "AssignTickets",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiAssignTicketsResponse"
}
},
"404": {
"description": "Returned when the resource does not exist.",
"schema": {
"format": "string"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiAssignTicketsRequest"
}
}
],
"tags": [
"Backend"
]
}
}
},
"definitions": {
"apiAssignTicketsRequest": {
"type": "object",
"properties": {
"ticket_id": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of Ticket IDs for which the Assignment is to be made."
},
"assignment": {
"$ref": "#/definitions/apiAssignment",
"description": "Assignment to be associated with the Ticket IDs."
}
}
},
"apiAssignTicketsResponse": {
"type": "object"
},
"apiAssignment": {
"type": "object",
"properties": {
"connection": {
"type": "string",
"description": "Connection information for this Assignment."
},
"properties": {
"type": "string",
"description": "Other details to be sent to the players. (Optional)\nOpen Match does not interpret these properties."
},
"error": {
"type": "string",
"description": "Error when finding an Assignment for this Ticket."
}
},
"description": "An Assignment object represents the assignment associated with a Ticket."
},
"apiFetchMatchesRequest": {
"type": "object",
"properties": {
"config": {
"$ref": "#/definitions/apiFunctionConfig",
"title": "Configuration of the MatchFunction to be executed for the given list of MatchProfiles"
},
"profile": {
"type": "array",
"items": {
"$ref": "#/definitions/apiMatchProfile"
},
"description": "MatchProfiles for which this MatchFunction should be executed."
}
}
},
"apiFetchMatchesResponse": {
"type": "object",
"properties": {
"match": {
"$ref": "#/definitions/apiMatch",
"description": "Result Match for the requested MatchProfile."
}
}
},
"apiFilter": {
"type": "object",
"properties": {
"attribute": {
"type": "string",
"description": "Name of the ticket attribute this Filter operates on."
},
"max": {
"type": "number",
"format": "double",
"description": "Maximum value. Defaults to positive infinity (any value above minv)."
},
"min": {
"type": "number",
"format": "double",
"description": "Minimum value. Defaults to 0."
}
},
"description": "A hard filter used to query a subset of Tickets meeting the filtering\ncriteria."
},
"apiFunctionConfig": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "A developer-chosen human-readable name for this Match Function."
},
"grpc": {
"$ref": "#/definitions/apiGrpcFunctionConfig"
},
"rest": {
"$ref": "#/definitions/apiRestFunctionConfig"
}
},
"description": "Configuration for the Match Function to be triggered by Open Match to\ngenerate proposals."
},
"apiGrpcFunctionConfig": {
"type": "object",
"properties": {
"host": {
"type": "string"
},
"port": {
"type": "integer",
"format": "int32"
}
},
"title": "Configuration for a GRPC Match Function"
},
"apiMatch": {
"type": "object",
"properties": {
"match_id": {
"type": "string",
"description": "A Match ID that should be passed through the stack for tracing."
},
"match_profile": {
"type": "string",
"description": "Name of the match profile that generated this Match."
},
"match_function": {
"type": "string",
"description": "Name of the match function that generated this Match."
},
"ticket": {
"type": "array",
"items": {
"$ref": "#/definitions/apiTicket"
},
"description": "Tickets belonging to this match."
},
"roster": {
"type": "array",
"items": {
"$ref": "#/definitions/apiRoster"
},
"title": "Set of Rosters that comprise this Match"
},
"properties": {
"$ref": "#/definitions/protobufStruct",
"description": "Match properties for this Match. Open Match does not interpret this field."
}
},
"description": "A Match is used to represent a completed match object. It can be generated by\na MatchFunction as a proposal or can be returned by OpenMatch as a result in\nresponse to the FetchMatches call."
},
"apiMatchProfile": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of this match profile."
},
"properties": {
"$ref": "#/definitions/protobufStruct",
"description": "Set of properties associated with this MatchProfile. (Optional)\nOpen Match does not interpret these properties but passes them through to\nthe MatchFunction."
},
"pool": {
"type": "array",
"items": {
"$ref": "#/definitions/apiPool"
},
"description": "Set of pools to be queried when generating a match for this MatchProfile.\nThe pool names can be used in empty Rosters to specify composition of a\nmatch."
},
"roster": {
"type": "array",
"items": {
"$ref": "#/definitions/apiRoster"
},
"description": "Set of Rosters for this match request. Could be empty Rosters used to\nindicate the composition of the generated Match or they could be partially\npre-populated Ticket list to be used in scenarios such as backfill / join\nin progress."
}
},
"description": "A MatchProfile is Open Match's representation of a Match specification. It is\nused to indicate the criteria for selecting players for a match. A\nMatchProfile is the input to the API to get matches and is passed to the\nMatchFunction. It contains all the information required by the MatchFunction\nto generate match proposals."
},
"apiPool": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "A developer-chosen human-readable name for this Pool."
},
"filter": {
"type": "array",
"items": {
"$ref": "#/definitions/apiFilter"
},
"description": "Set of Filters indicating the filtering criteria. Selected players must\nmatch every Filter."
}
}
},
"apiRestFunctionConfig": {
"type": "object",
"properties": {
"host": {
"type": "string"
},
"port": {
"type": "integer",
"format": "int32"
}
},
"description": "Configuration for a REST Match Function."
},
"apiRoster": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "A developer-chosen human-readable name for this Roster."
},
"ticket_id": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tickets belonging to this Roster."
}
},
"description": "A Roster is a named collection of Ticket IDs. It exists so that a Tickets\nassociated with a Match can be labelled to belong to a team, sub-team etc. It\ncan also be used to represent the current state of a Match in scenarios such\nas backfill, join-in-progress etc."
},
"apiTicket": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The Ticket ID generated by Open Match."
},
"properties": {
"$ref": "#/definitions/protobufStruct",
"description": "Properties contains custom info about the ticket. Top level values can be\nused in indexing and filtering to find tickets."
},
"assignment": {
"$ref": "#/definitions/apiAssignment",
"description": "Assignment associated with the Ticket."
}
},
"description": "A Ticket is a basic matchmaking entity in Open Match. In order to enter\nmatchmaking using Open Match, the client should generate a Ticket, passing in\nthe properties to be associated with this Ticket. Open Match will generate an\nID for a Ticket during creation. A Ticket could be used to represent an\nindividual 'Player' or a 'Group' of players. Open Match will not interpret\nwhat the Ticket represents but just treat it as a matchmaking unit with a set\nof properties. Open Match stores the Ticket in state storage and enables an\nAssignment to be associated with this Ticket."
},
"protobufAny": {
"type": "object",
"properties": {
"type_url": {
"type": "string",
"description": "A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. This string must contain at least\none \"/\" character. The last segment of the URL's path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading \".\" is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics."
},
"value": {
"type": "string",
"format": "byte",
"description": "Must be a valid serialized protocol buffer of the above specified type."
}
},
"description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\n Example 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := ptypes.MarshalAny(foo)\n ...\n foo := \u0026pb.Foo{}\n if err := ptypes.UnmarshalAny(any, foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }"
},
"protobufListValue": {
"type": "object",
"properties": {
"values": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufValue"
},
"description": "Repeated field of dynamically typed values."
}
},
"description": "`ListValue` is a wrapper around a repeated field of values.\n\nThe JSON representation for `ListValue` is JSON array."
},
"protobufNullValue": {
"type": "string",
"enum": [
"NULL_VALUE"
],
"default": "NULL_VALUE",
"description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value."
},
"protobufStruct": {
"type": "object",
"properties": {
"fields": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/protobufValue"
},
"description": "Unordered map of dynamically typed values."
}
},
"description": "`Struct` represents a structured data value, consisting of fields\nwhich map to dynamically typed values. In some languages, `Struct`\nmight be supported by a native representation. For example, in\nscripting languages like JS a struct is represented as an\nobject. The details of that representation are described together\nwith the proto support for the language.\n\nThe JSON representation for `Struct` is JSON object."
},
"protobufValue": {
"type": "object",
"properties": {
"null_value": {
"$ref": "#/definitions/protobufNullValue",
"description": "Represents a null value."
},
"number_value": {
"type": "number",
"format": "double",
"description": "Represents a double value."
},
"string_value": {
"type": "string",
"description": "Represents a string value."
},
"bool_value": {
"type": "boolean",
"format": "boolean",
"description": "Represents a boolean value."
},
"struct_value": {
"$ref": "#/definitions/protobufStruct",
"description": "Represents a structured value."
},
"list_value": {
"$ref": "#/definitions/protobufListValue",
"description": "Represents a repeated `Value`."
}
},
"description": "`Value` represents a dynamically typed value which can be either\nnull, a number, a string, a boolean, a recursive struct value, or a\nlist of values. A producer of value is expected to set one of that\nvariants, absence of any variant indicates an error.\n\nThe JSON representation for `Value` is JSON value."
},
"runtimeStreamError": {
"type": "object",
"properties": {
"grpc_code": {
"type": "integer",
"format": "int32"
},
"http_code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"http_status": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
},
"x-stream-definitions": {
"apiFetchMatchesResponse": {
"type": "object",
"properties": {
"result": {
"$ref": "#/definitions/apiFetchMatchesResponse"
},
"error": {
"$ref": "#/definitions/runtimeStreamError"
}
},
"title": "Stream result of apiFetchMatchesResponse"
}
},
"externalDocs": {
"description": "Open Match Documentation",
"url": "https://open-match.dev/site/docs/"
}
}

79
api/evaluator.proto Normal file
View File

@ -0,0 +1,79 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package api;
option go_package = "internal/pb";
import "api/messages.proto";
import "google/api/annotations.proto";
import "protoc-gen-swagger/options/annotations.proto";
option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
info: {
title: "Evaluator"
version: "1.0"
contact: {
name: "Open Match"
url: "https://open-match.dev"
email: "open-match-discuss@googlegroups.com"
}
license: {
name: "Apache 2.0 License"
url: "https://github.com/googleforgames/open-match/blob/master/LICENSE"
}
}
external_docs: {
url: "https://open-match.dev/site/docs/"
description: "Open Match Documentation"
}
schemes: HTTP
schemes: HTTPS
consumes: "application/json"
produces: "application/json"
responses: {
key: "404"
value: {
description: "Returned when the resource does not exist."
schema: { json_schema: { type: STRING } }
}
}
// TODO Add annotations for security_defintiions.
// See
// https://github.com/grpc-ecosystem/grpc-gateway/blob/master/examples/proto/examplepb/a_bit_of_everything.proto
};
message EvaluateRequest {
// List of Matches to evaluate.
repeated Match match = 1;
}
message EvaluateResponse {
// Accepted list of Matches.
repeated Match match = 1;
}
// The service implementing the Evaluator API that is called to evaluate
// matches generated by MMFs and shortlist them to accepted results.
service Evaluator {
// Evaluate accepts a list of matches, triggers the user configured evaluation
// function with these and other matches in the evaluation window and returns
// matches that are accepted by the Evaluator as valid results.
rpc Evaluate(EvaluateRequest) returns (EvaluateResponse) {
option (google.api.http) = {
post: "/v1/evaluator/matches:evaluate"
body: "*"
};
}
}

246
api/evaluator.swagger.json Normal file
View File

@ -0,0 +1,246 @@
{
"swagger": "2.0",
"info": {
"title": "Evaluator",
"version": "1.0",
"contact": {
"name": "Open Match",
"url": "https://open-match.dev",
"email": "open-match-discuss@googlegroups.com"
},
"license": {
"name": "Apache 2.0 License",
"url": "https://github.com/googleforgames/open-match/blob/master/LICENSE"
}
},
"schemes": [
"http",
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/evaluator/matches:evaluate": {
"post": {
"summary": "Evaluate accepts a list of matches, triggers the user configured evaluation\nfunction with these and other matches in the evaluation window and returns\nmatches that are accepted by the Evaluator as valid results.",
"operationId": "Evaluate",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiEvaluateResponse"
}
},
"404": {
"description": "Returned when the resource does not exist.",
"schema": {
"format": "string"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiEvaluateRequest"
}
}
],
"tags": [
"Evaluator"
]
}
}
},
"definitions": {
"apiAssignment": {
"type": "object",
"properties": {
"connection": {
"type": "string",
"description": "Connection information for this Assignment."
},
"properties": {
"type": "string",
"description": "Other details to be sent to the players. (Optional)\nOpen Match does not interpret these properties."
},
"error": {
"type": "string",
"description": "Error when finding an Assignment for this Ticket."
}
},
"description": "An Assignment object represents the assignment associated with a Ticket."
},
"apiEvaluateRequest": {
"type": "object",
"properties": {
"match": {
"type": "array",
"items": {
"$ref": "#/definitions/apiMatch"
},
"description": "List of Matches to evaluate."
}
}
},
"apiEvaluateResponse": {
"type": "object",
"properties": {
"match": {
"type": "array",
"items": {
"$ref": "#/definitions/apiMatch"
},
"description": "Accepted list of Matches."
}
}
},
"apiMatch": {
"type": "object",
"properties": {
"match_id": {
"type": "string",
"description": "A Match ID that should be passed through the stack for tracing."
},
"match_profile": {
"type": "string",
"description": "Name of the match profile that generated this Match."
},
"match_function": {
"type": "string",
"description": "Name of the match function that generated this Match."
},
"ticket": {
"type": "array",
"items": {
"$ref": "#/definitions/apiTicket"
},
"description": "Tickets belonging to this match."
},
"roster": {
"type": "array",
"items": {
"$ref": "#/definitions/apiRoster"
},
"title": "Set of Rosters that comprise this Match"
},
"properties": {
"$ref": "#/definitions/protobufStruct",
"description": "Match properties for this Match. Open Match does not interpret this field."
}
},
"description": "A Match is used to represent a completed match object. It can be generated by\na MatchFunction as a proposal or can be returned by OpenMatch as a result in\nresponse to the FetchMatches call."
},
"apiRoster": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "A developer-chosen human-readable name for this Roster."
},
"ticket_id": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tickets belonging to this Roster."
}
},
"description": "A Roster is a named collection of Ticket IDs. It exists so that a Tickets\nassociated with a Match can be labelled to belong to a team, sub-team etc. It\ncan also be used to represent the current state of a Match in scenarios such\nas backfill, join-in-progress etc."
},
"apiTicket": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The Ticket ID generated by Open Match."
},
"properties": {
"$ref": "#/definitions/protobufStruct",
"description": "Properties contains custom info about the ticket. Top level values can be\nused in indexing and filtering to find tickets."
},
"assignment": {
"$ref": "#/definitions/apiAssignment",
"description": "Assignment associated with the Ticket."
}
},
"description": "A Ticket is a basic matchmaking entity in Open Match. In order to enter\nmatchmaking using Open Match, the client should generate a Ticket, passing in\nthe properties to be associated with this Ticket. Open Match will generate an\nID for a Ticket during creation. A Ticket could be used to represent an\nindividual 'Player' or a 'Group' of players. Open Match will not interpret\nwhat the Ticket represents but just treat it as a matchmaking unit with a set\nof properties. Open Match stores the Ticket in state storage and enables an\nAssignment to be associated with this Ticket."
},
"protobufListValue": {
"type": "object",
"properties": {
"values": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufValue"
},
"description": "Repeated field of dynamically typed values."
}
},
"description": "`ListValue` is a wrapper around a repeated field of values.\n\nThe JSON representation for `ListValue` is JSON array."
},
"protobufNullValue": {
"type": "string",
"enum": [
"NULL_VALUE"
],
"default": "NULL_VALUE",
"description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value."
},
"protobufStruct": {
"type": "object",
"properties": {
"fields": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/protobufValue"
},
"description": "Unordered map of dynamically typed values."
}
},
"description": "`Struct` represents a structured data value, consisting of fields\nwhich map to dynamically typed values. In some languages, `Struct`\nmight be supported by a native representation. For example, in\nscripting languages like JS a struct is represented as an\nobject. The details of that representation are described together\nwith the proto support for the language.\n\nThe JSON representation for `Struct` is JSON object."
},
"protobufValue": {
"type": "object",
"properties": {
"null_value": {
"$ref": "#/definitions/protobufNullValue",
"description": "Represents a null value."
},
"number_value": {
"type": "number",
"format": "double",
"description": "Represents a double value."
},
"string_value": {
"type": "string",
"description": "Represents a string value."
},
"bool_value": {
"type": "boolean",
"format": "boolean",
"description": "Represents a boolean value."
},
"struct_value": {
"$ref": "#/definitions/protobufStruct",
"description": "Represents a structured value."
},
"list_value": {
"$ref": "#/definitions/protobufListValue",
"description": "Represents a repeated `Value`."
}
},
"description": "`Value` represents a dynamically typed value which can be either\nnull, a number, a string, a boolean, a recursive struct value, or a\nlist of values. A producer of value is expected to set one of that\nvariants, absence of any variant indicates an error.\n\nThe JSON representation for `Value` is JSON value."
}
},
"externalDocs": {
"description": "Open Match Documentation",
"url": "https://open-match.dev/site/docs/"
}
}

128
api/frontend.proto Normal file
View File

@ -0,0 +1,128 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package api;
option go_package = "internal/pb";
import "api/messages.proto";
import "google/api/annotations.proto";
import "protoc-gen-swagger/options/annotations.proto";
option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
info: {
title: "Frontend"
version: "1.0"
contact: {
name: "Open Match"
url: "https://open-match.dev"
email: "open-match-discuss@googlegroups.com"
}
license: {
name: "Apache 2.0 License"
url: "https://github.com/googleforgames/open-match/blob/master/LICENSE"
}
}
external_docs: {
url: "https://open-match.dev/site/docs/"
description: "Open Match Documentation"
}
schemes: HTTP
schemes: HTTPS
consumes: "application/json"
produces: "application/json"
responses: {
key: "404"
value: {
description: "Returned when the resource does not exist."
schema: { json_schema: { type: STRING } }
}
}
// TODO Add annotations for security_defintiions.
// See
// https://github.com/grpc-ecosystem/grpc-gateway/blob/master/examples/proto/examplepb/a_bit_of_everything.proto
};
message CreateTicketRequest {
// Ticket object with the properties of the Ticket to be created.
Ticket ticket = 1;
}
message CreateTicketResponse {
// Ticket object for the created Ticket - with the ticket ID populated.
Ticket ticket = 1;
}
message DeleteTicketRequest {
// Ticket ID of the Ticket to be deleted.
string ticket_id = 1;
}
message DeleteTicketResponse {}
message GetTicketRequest {
// Ticket ID of the Ticket to fetch.
string ticket_id = 1;
}
message GetAssignmentsRequest {
// Ticket ID of the Ticket to get updates on.
string ticket_id = 1;
}
message GetAssignmentsResponse {
// The updated Ticket object.
Assignment assignment = 1;
}
// The Frontend service enables creating Tickets for matchmaking and fetching
// the status of these Tickets.
service Frontend {
// CreateTicket will create a new ticket, assign a Ticket ID to it and put the
// Ticket in state storage. It will then look through the 'properties' field
// for the attributes defined as indices the matchmakaking config. If the
// attributes exist and are valid integers, they will be indexed. Creating a
// ticket adds the Ticket to the pool of Tickets considered for matchmaking.
rpc CreateTicket(CreateTicketRequest) returns (CreateTicketResponse) {
option (google.api.http) = {
post: "/v1/frontend/tickets"
body: "*"
};
}
// DeleteTicket removes the Ticket from state storage and from corresponding
// configured indices. Deleting the ticket stops the ticket from being
// considered for future matchmaking requests.
rpc DeleteTicket(DeleteTicketRequest) returns (DeleteTicketResponse) {
option (google.api.http) = {
delete: "/v1/frontend/tickets/{ticket_id}"
};
}
// GetTicket fetches the ticket associated with the specified Ticket ID.
rpc GetTicket(GetTicketRequest) returns (Ticket) {
option (google.api.http) = {
get: "/v1/frontend/tickets/{ticket_id}"
};
}
// GetAssignments streams matchmaking results from Open Match for the
// provided Ticket ID.
rpc GetAssignments(GetAssignmentsRequest)
returns (stream GetAssignmentsResponse) {
option (google.api.http) = {
get: "/v1/frontend/tickets/{ticket_id}/assignments"
};
}
}

347
api/frontend.swagger.json Normal file
View File

@ -0,0 +1,347 @@
{
"swagger": "2.0",
"info": {
"title": "Frontend",
"version": "1.0",
"contact": {
"name": "Open Match",
"url": "https://open-match.dev",
"email": "open-match-discuss@googlegroups.com"
},
"license": {
"name": "Apache 2.0 License",
"url": "https://github.com/googleforgames/open-match/blob/master/LICENSE"
}
},
"schemes": [
"http",
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/frontend/tickets": {
"post": {
"summary": "CreateTicket will create a new ticket, assign a Ticket ID to it and put the\nTicket in state storage. It will then look through the 'properties' field\nfor the attributes defined as indices the matchmakaking config. If the\nattributes exist and are valid integers, they will be indexed. Creating a\nticket adds the Ticket to the pool of Tickets considered for matchmaking.",
"operationId": "CreateTicket",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiCreateTicketResponse"
}
},
"404": {
"description": "Returned when the resource does not exist.",
"schema": {
"format": "string"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiCreateTicketRequest"
}
}
],
"tags": [
"Frontend"
]
}
},
"/v1/frontend/tickets/{ticket_id}": {
"get": {
"summary": "GetTicket fetches the ticket associated with the specified Ticket ID.",
"operationId": "GetTicket",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiTicket"
}
},
"404": {
"description": "Returned when the resource does not exist.",
"schema": {
"format": "string"
}
}
},
"parameters": [
{
"name": "ticket_id",
"description": "Ticket ID of the Ticket to fetch.",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"Frontend"
]
},
"delete": {
"summary": "DeleteTicket removes the Ticket from state storage and from corresponding\nconfigured indices. Deleting the ticket stops the ticket from being\nconsidered for future matchmaking requests.",
"operationId": "DeleteTicket",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiDeleteTicketResponse"
}
},
"404": {
"description": "Returned when the resource does not exist.",
"schema": {
"format": "string"
}
}
},
"parameters": [
{
"name": "ticket_id",
"description": "Ticket ID of the Ticket to be deleted.",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"Frontend"
]
}
},
"/v1/frontend/tickets/{ticket_id}/assignments": {
"get": {
"summary": "GetAssignments streams matchmaking results from Open Match for the\nprovided Ticket ID.",
"operationId": "GetAssignments",
"responses": {
"200": {
"description": "A successful response.(streaming responses)",
"schema": {
"$ref": "#/x-stream-definitions/apiGetAssignmentsResponse"
}
},
"404": {
"description": "Returned when the resource does not exist.",
"schema": {
"format": "string"
}
}
},
"parameters": [
{
"name": "ticket_id",
"description": "Ticket ID of the Ticket to get updates on.",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"Frontend"
]
}
}
},
"definitions": {
"apiAssignment": {
"type": "object",
"properties": {
"connection": {
"type": "string",
"description": "Connection information for this Assignment."
},
"properties": {
"type": "string",
"description": "Other details to be sent to the players. (Optional)\nOpen Match does not interpret these properties."
},
"error": {
"type": "string",
"description": "Error when finding an Assignment for this Ticket."
}
},
"description": "An Assignment object represents the assignment associated with a Ticket."
},
"apiCreateTicketRequest": {
"type": "object",
"properties": {
"ticket": {
"$ref": "#/definitions/apiTicket",
"description": "Ticket object with the properties of the Ticket to be created."
}
}
},
"apiCreateTicketResponse": {
"type": "object",
"properties": {
"ticket": {
"$ref": "#/definitions/apiTicket",
"description": "Ticket object for the created Ticket - with the ticket ID populated."
}
}
},
"apiDeleteTicketResponse": {
"type": "object"
},
"apiGetAssignmentsResponse": {
"type": "object",
"properties": {
"assignment": {
"$ref": "#/definitions/apiAssignment",
"description": "The updated Ticket object."
}
}
},
"apiTicket": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The Ticket ID generated by Open Match."
},
"properties": {
"$ref": "#/definitions/protobufStruct",
"description": "Properties contains custom info about the ticket. Top level values can be\nused in indexing and filtering to find tickets."
},
"assignment": {
"$ref": "#/definitions/apiAssignment",
"description": "Assignment associated with the Ticket."
}
},
"description": "A Ticket is a basic matchmaking entity in Open Match. In order to enter\nmatchmaking using Open Match, the client should generate a Ticket, passing in\nthe properties to be associated with this Ticket. Open Match will generate an\nID for a Ticket during creation. A Ticket could be used to represent an\nindividual 'Player' or a 'Group' of players. Open Match will not interpret\nwhat the Ticket represents but just treat it as a matchmaking unit with a set\nof properties. Open Match stores the Ticket in state storage and enables an\nAssignment to be associated with this Ticket."
},
"protobufAny": {
"type": "object",
"properties": {
"type_url": {
"type": "string",
"description": "A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. This string must contain at least\none \"/\" character. The last segment of the URL's path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading \".\" is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics."
},
"value": {
"type": "string",
"format": "byte",
"description": "Must be a valid serialized protocol buffer of the above specified type."
}
},
"description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\n Example 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := ptypes.MarshalAny(foo)\n ...\n foo := \u0026pb.Foo{}\n if err := ptypes.UnmarshalAny(any, foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }"
},
"protobufListValue": {
"type": "object",
"properties": {
"values": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufValue"
},
"description": "Repeated field of dynamically typed values."
}
},
"description": "`ListValue` is a wrapper around a repeated field of values.\n\nThe JSON representation for `ListValue` is JSON array."
},
"protobufNullValue": {
"type": "string",
"enum": [
"NULL_VALUE"
],
"default": "NULL_VALUE",
"description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value."
},
"protobufStruct": {
"type": "object",
"properties": {
"fields": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/protobufValue"
},
"description": "Unordered map of dynamically typed values."
}
},
"description": "`Struct` represents a structured data value, consisting of fields\nwhich map to dynamically typed values. In some languages, `Struct`\nmight be supported by a native representation. For example, in\nscripting languages like JS a struct is represented as an\nobject. The details of that representation are described together\nwith the proto support for the language.\n\nThe JSON representation for `Struct` is JSON object."
},
"protobufValue": {
"type": "object",
"properties": {
"null_value": {
"$ref": "#/definitions/protobufNullValue",
"description": "Represents a null value."
},
"number_value": {
"type": "number",
"format": "double",
"description": "Represents a double value."
},
"string_value": {
"type": "string",
"description": "Represents a string value."
},
"bool_value": {
"type": "boolean",
"format": "boolean",
"description": "Represents a boolean value."
},
"struct_value": {
"$ref": "#/definitions/protobufStruct",
"description": "Represents a structured value."
},
"list_value": {
"$ref": "#/definitions/protobufListValue",
"description": "Represents a repeated `Value`."
}
},
"description": "`Value` represents a dynamically typed value which can be either\nnull, a number, a string, a boolean, a recursive struct value, or a\nlist of values. A producer of value is expected to set one of that\nvariants, absence of any variant indicates an error.\n\nThe JSON representation for `Value` is JSON value."
},
"runtimeStreamError": {
"type": "object",
"properties": {
"grpc_code": {
"type": "integer",
"format": "int32"
},
"http_code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"http_status": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
},
"x-stream-definitions": {
"apiGetAssignmentsResponse": {
"type": "object",
"properties": {
"result": {
"$ref": "#/definitions/apiGetAssignmentsResponse"
},
"error": {
"$ref": "#/definitions/runtimeStreamError"
}
},
"title": "Stream result of apiGetAssignmentsResponse"
}
},
"externalDocs": {
"description": "Open Match Documentation",
"url": "https://open-match.dev/site/docs/"
}
}

79
api/matchfunction.proto Normal file
View File

@ -0,0 +1,79 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package api;
option go_package = "internal/pb";
import "api/messages.proto";
import "google/api/annotations.proto";
import "protoc-gen-swagger/options/annotations.proto";
option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
info: {
title: "Match Function"
version: "1.0"
contact: {
name: "Open Match"
url: "https://open-match.dev"
email: "open-match-discuss@googlegroups.com"
}
license: {
name: "Apache 2.0 License"
url: "https://github.com/googleforgames/open-match/blob/master/LICENSE"
}
}
external_docs: {
url: "https://open-match.dev/site/docs/"
description: "Open Match Documentation"
}
schemes: HTTP
schemes: HTTPS
consumes: "application/json"
produces: "application/json"
responses: {
key: "404"
value: {
description: "Returned when the resource does not exist."
schema: { json_schema: { type: STRING } }
}
}
// TODO Add annotations for security_defintiions.
// See
// https://github.com/grpc-ecosystem/grpc-gateway/blob/master/examples/proto/examplepb/a_bit_of_everything.proto
};
message RunRequest {
// The MatchProfile that describes the Match that this MatchFunction needs to
// generate proposals for.
MatchProfile profile = 1;
}
message RunResponse {
// The proposal generated by this MatchFunction Run.
repeated Match proposal = 1;
}
// This proto defines the API for running Match Functions as long-lived,
// 'serving' functions.
service MatchFunction {
// This is the function that is executed when by the Open Match backend to
// generate Match proposals.
rpc Run(RunRequest) returns (RunResponse) {
option (google.api.http) = {
post: "/v1/matchfunction:run"
body: "*"
};
}
}

View File

@ -0,0 +1,307 @@
{
"swagger": "2.0",
"info": {
"title": "Match Function",
"version": "1.0",
"contact": {
"name": "Open Match",
"url": "https://open-match.dev",
"email": "open-match-discuss@googlegroups.com"
},
"license": {
"name": "Apache 2.0 License",
"url": "https://github.com/googleforgames/open-match/blob/master/LICENSE"
}
},
"schemes": [
"http",
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/matchfunction:run": {
"post": {
"summary": "This is the function that is executed when by the Open Match backend to\ngenerate Match proposals.",
"operationId": "Run",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiRunResponse"
}
},
"404": {
"description": "Returned when the resource does not exist.",
"schema": {
"format": "string"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiRunRequest"
}
}
],
"tags": [
"MatchFunction"
]
}
}
},
"definitions": {
"apiAssignment": {
"type": "object",
"properties": {
"connection": {
"type": "string",
"description": "Connection information for this Assignment."
},
"properties": {
"type": "string",
"description": "Other details to be sent to the players. (Optional)\nOpen Match does not interpret these properties."
},
"error": {
"type": "string",
"description": "Error when finding an Assignment for this Ticket."
}
},
"description": "An Assignment object represents the assignment associated with a Ticket."
},
"apiFilter": {
"type": "object",
"properties": {
"attribute": {
"type": "string",
"description": "Name of the ticket attribute this Filter operates on."
},
"max": {
"type": "number",
"format": "double",
"description": "Maximum value. Defaults to positive infinity (any value above minv)."
},
"min": {
"type": "number",
"format": "double",
"description": "Minimum value. Defaults to 0."
}
},
"description": "A hard filter used to query a subset of Tickets meeting the filtering\ncriteria."
},
"apiMatch": {
"type": "object",
"properties": {
"match_id": {
"type": "string",
"description": "A Match ID that should be passed through the stack for tracing."
},
"match_profile": {
"type": "string",
"description": "Name of the match profile that generated this Match."
},
"match_function": {
"type": "string",
"description": "Name of the match function that generated this Match."
},
"ticket": {
"type": "array",
"items": {
"$ref": "#/definitions/apiTicket"
},
"description": "Tickets belonging to this match."
},
"roster": {
"type": "array",
"items": {
"$ref": "#/definitions/apiRoster"
},
"title": "Set of Rosters that comprise this Match"
},
"properties": {
"$ref": "#/definitions/protobufStruct",
"description": "Match properties for this Match. Open Match does not interpret this field."
}
},
"description": "A Match is used to represent a completed match object. It can be generated by\na MatchFunction as a proposal or can be returned by OpenMatch as a result in\nresponse to the FetchMatches call."
},
"apiMatchProfile": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of this match profile."
},
"properties": {
"$ref": "#/definitions/protobufStruct",
"description": "Set of properties associated with this MatchProfile. (Optional)\nOpen Match does not interpret these properties but passes them through to\nthe MatchFunction."
},
"pool": {
"type": "array",
"items": {
"$ref": "#/definitions/apiPool"
},
"description": "Set of pools to be queried when generating a match for this MatchProfile.\nThe pool names can be used in empty Rosters to specify composition of a\nmatch."
},
"roster": {
"type": "array",
"items": {
"$ref": "#/definitions/apiRoster"
},
"description": "Set of Rosters for this match request. Could be empty Rosters used to\nindicate the composition of the generated Match or they could be partially\npre-populated Ticket list to be used in scenarios such as backfill / join\nin progress."
}
},
"description": "A MatchProfile is Open Match's representation of a Match specification. It is\nused to indicate the criteria for selecting players for a match. A\nMatchProfile is the input to the API to get matches and is passed to the\nMatchFunction. It contains all the information required by the MatchFunction\nto generate match proposals."
},
"apiPool": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "A developer-chosen human-readable name for this Pool."
},
"filter": {
"type": "array",
"items": {
"$ref": "#/definitions/apiFilter"
},
"description": "Set of Filters indicating the filtering criteria. Selected players must\nmatch every Filter."
}
}
},
"apiRoster": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "A developer-chosen human-readable name for this Roster."
},
"ticket_id": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tickets belonging to this Roster."
}
},
"description": "A Roster is a named collection of Ticket IDs. It exists so that a Tickets\nassociated with a Match can be labelled to belong to a team, sub-team etc. It\ncan also be used to represent the current state of a Match in scenarios such\nas backfill, join-in-progress etc."
},
"apiRunRequest": {
"type": "object",
"properties": {
"profile": {
"$ref": "#/definitions/apiMatchProfile",
"description": "The MatchProfile that describes the Match that this MatchFunction needs to\ngenerate proposals for."
}
}
},
"apiRunResponse": {
"type": "object",
"properties": {
"proposal": {
"type": "array",
"items": {
"$ref": "#/definitions/apiMatch"
},
"description": "The proposal generated by this MatchFunction Run."
}
}
},
"apiTicket": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The Ticket ID generated by Open Match."
},
"properties": {
"$ref": "#/definitions/protobufStruct",
"description": "Properties contains custom info about the ticket. Top level values can be\nused in indexing and filtering to find tickets."
},
"assignment": {
"$ref": "#/definitions/apiAssignment",
"description": "Assignment associated with the Ticket."
}
},
"description": "A Ticket is a basic matchmaking entity in Open Match. In order to enter\nmatchmaking using Open Match, the client should generate a Ticket, passing in\nthe properties to be associated with this Ticket. Open Match will generate an\nID for a Ticket during creation. A Ticket could be used to represent an\nindividual 'Player' or a 'Group' of players. Open Match will not interpret\nwhat the Ticket represents but just treat it as a matchmaking unit with a set\nof properties. Open Match stores the Ticket in state storage and enables an\nAssignment to be associated with this Ticket."
},
"protobufListValue": {
"type": "object",
"properties": {
"values": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufValue"
},
"description": "Repeated field of dynamically typed values."
}
},
"description": "`ListValue` is a wrapper around a repeated field of values.\n\nThe JSON representation for `ListValue` is JSON array."
},
"protobufNullValue": {
"type": "string",
"enum": [
"NULL_VALUE"
],
"default": "NULL_VALUE",
"description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value."
},
"protobufStruct": {
"type": "object",
"properties": {
"fields": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/protobufValue"
},
"description": "Unordered map of dynamically typed values."
}
},
"description": "`Struct` represents a structured data value, consisting of fields\nwhich map to dynamically typed values. In some languages, `Struct`\nmight be supported by a native representation. For example, in\nscripting languages like JS a struct is represented as an\nobject. The details of that representation are described together\nwith the proto support for the language.\n\nThe JSON representation for `Struct` is JSON object."
},
"protobufValue": {
"type": "object",
"properties": {
"null_value": {
"$ref": "#/definitions/protobufNullValue",
"description": "Represents a null value."
},
"number_value": {
"type": "number",
"format": "double",
"description": "Represents a double value."
},
"string_value": {
"type": "string",
"description": "Represents a string value."
},
"bool_value": {
"type": "boolean",
"format": "boolean",
"description": "Represents a boolean value."
},
"struct_value": {
"$ref": "#/definitions/protobufStruct",
"description": "Represents a structured value."
},
"list_value": {
"$ref": "#/definitions/protobufListValue",
"description": "Represents a repeated `Value`."
}
},
"description": "`Value` represents a dynamically typed value which can be either\nnull, a number, a string, a boolean, a recursive struct value, or a\nlist of values. A producer of value is expected to set one of that\nvariants, absence of any variant indicates an error.\n\nThe JSON representation for `Value` is JSON value."
}
},
"externalDocs": {
"description": "Open Match Documentation",
"url": "https://open-match.dev/site/docs/"
}
}

135
api/messages.proto Normal file
View File

@ -0,0 +1,135 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package api;
option go_package = "internal/pb";
import "google/protobuf/struct.proto";
// A Ticket is a basic matchmaking entity in Open Match. In order to enter
// matchmaking using Open Match, the client should generate a Ticket, passing in
// the properties to be associated with this Ticket. Open Match will generate an
// ID for a Ticket during creation. A Ticket could be used to represent an
// individual 'Player' or a 'Group' of players. Open Match will not interpret
// what the Ticket represents but just treat it as a matchmaking unit with a set
// of properties. Open Match stores the Ticket in state storage and enables an
// Assignment to be associated with this Ticket.
message Ticket {
// The Ticket ID generated by Open Match.
string id = 1;
// Properties contains custom info about the ticket. Top level values can be
// used in indexing and filtering to find tickets.
google.protobuf.Struct properties = 2;
// Assignment associated with the Ticket.
Assignment assignment = 3;
}
// An Assignment object represents the assignment associated with a Ticket.
message Assignment {
// Connection information for this Assignment.
string connection = 1;
// Other details to be sent to the players. (Optional)
// Open Match does not interpret these properties.
string properties = 2;
// Error when finding an Assignment for this Ticket.
string error = 3;
}
// A hard filter used to query a subset of Tickets meeting the filtering
// criteria.
message Filter {
// Name of the ticket attribute this Filter operates on.
string attribute = 1;
// Maximum value. Defaults to positive infinity (any value above minv).
double max = 2;
// Minimum value. Defaults to 0.
double min = 3;
}
message Pool {
// A developer-chosen human-readable name for this Pool.
string name = 1;
// Set of Filters indicating the filtering criteria. Selected players must
// match every Filter.
repeated Filter filter = 2;
}
// A Roster is a named collection of Ticket IDs. It exists so that a Tickets
// associated with a Match can be labelled to belong to a team, sub-team etc. It
// can also be used to represent the current state of a Match in scenarios such
// as backfill, join-in-progress etc.
message Roster {
// A developer-chosen human-readable name for this Roster.
string name = 1;
// Tickets belonging to this Roster.
repeated string ticket_id = 2;
}
// A MatchProfile is Open Match's representation of a Match specification. It is
// used to indicate the criteria for selecting players for a match. A
// MatchProfile is the input to the API to get matches and is passed to the
// MatchFunction. It contains all the information required by the MatchFunction
// to generate match proposals.
message MatchProfile {
// Name of this match profile.
string name = 1;
// Set of properties associated with this MatchProfile. (Optional)
// Open Match does not interpret these properties but passes them through to
// the MatchFunction.
google.protobuf.Struct properties = 2;
// Set of pools to be queried when generating a match for this MatchProfile.
// The pool names can be used in empty Rosters to specify composition of a
// match.
repeated Pool pool = 3;
// Set of Rosters for this match request. Could be empty Rosters used to
// indicate the composition of the generated Match or they could be partially
// pre-populated Ticket list to be used in scenarios such as backfill / join
// in progress.
repeated Roster roster = 4;
}
// A Match is used to represent a completed match object. It can be generated by
// a MatchFunction as a proposal or can be returned by OpenMatch as a result in
// response to the FetchMatches call.
message Match {
// A Match ID that should be passed through the stack for tracing.
string match_id = 1;
// Name of the match profile that generated this Match.
string match_profile = 2;
// Name of the match function that generated this Match.
string match_function = 3;
// Tickets belonging to this match.
repeated Ticket ticket = 4;
// Set of Rosters that comprise this Match
repeated Roster roster = 5;
// Match properties for this Match. Open Match does not interpret this field.
google.protobuf.Struct properties = 6;
}

78
api/mmlogic.proto Normal file
View File

@ -0,0 +1,78 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package api;
option go_package = "internal/pb";
import "api/messages.proto";
import "google/api/annotations.proto";
import "protoc-gen-swagger/options/annotations.proto";
option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
info: {
title: "MM Logic (Data Layer)"
version: "1.0"
contact: {
name: "Open Match"
url: "https://open-match.dev"
email: "open-match-discuss@googlegroups.com"
}
license: {
name: "Apache 2.0 License"
url: "https://github.com/googleforgames/open-match/blob/master/LICENSE"
}
}
external_docs: {
url: "https://open-match.dev/site/docs/"
description: "Open Match Documentation"
}
schemes: HTTP
schemes: HTTPS
consumes: "application/json"
produces: "application/json"
responses: {
key: "404"
value: {
description: "Returned when the resource does not exist."
schema: { json_schema: { type: STRING } }
}
}
// TODO Add annotations for security_defintiions.
// See
// https://github.com/grpc-ecosystem/grpc-gateway/blob/master/examples/proto/examplepb/a_bit_of_everything.proto
};
message QueryTicketsRequest {
// The Pool representing the set of Filters to be queried.
Pool pool = 1;
}
message QueryTicketsResponse {
// The Tickets that meet the Filter criteria requested by the Pool.
repeated Ticket ticket = 1;
}
// The MMLogic API provides utility functions for common MMF functionality such
// as retreiving Tickets from state storage.
service MmLogic {
// QueryTickets gets the list of Tickets that match every Filter in the
// specified Pool.
rpc QueryTickets(QueryTicketsRequest) returns (stream QueryTicketsResponse) {
option (google.api.http) = {
post: "/v1/mmlogic/tickets:query"
body: "*"
};
}
}

280
api/mmlogic.swagger.json Normal file
View File

@ -0,0 +1,280 @@
{
"swagger": "2.0",
"info": {
"title": "MM Logic (Data Layer)",
"version": "1.0",
"contact": {
"name": "Open Match",
"url": "https://open-match.dev",
"email": "open-match-discuss@googlegroups.com"
},
"license": {
"name": "Apache 2.0 License",
"url": "https://github.com/googleforgames/open-match/blob/master/LICENSE"
}
},
"schemes": [
"http",
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/mmlogic/tickets:query": {
"post": {
"summary": "QueryTickets gets the list of Tickets that match every Filter in the\nspecified Pool.",
"operationId": "QueryTickets",
"responses": {
"200": {
"description": "A successful response.(streaming responses)",
"schema": {
"$ref": "#/x-stream-definitions/apiQueryTicketsResponse"
}
},
"404": {
"description": "Returned when the resource does not exist.",
"schema": {
"format": "string"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiQueryTicketsRequest"
}
}
],
"tags": [
"MmLogic"
]
}
}
},
"definitions": {
"apiAssignment": {
"type": "object",
"properties": {
"connection": {
"type": "string",
"description": "Connection information for this Assignment."
},
"properties": {
"type": "string",
"description": "Other details to be sent to the players. (Optional)\nOpen Match does not interpret these properties."
},
"error": {
"type": "string",
"description": "Error when finding an Assignment for this Ticket."
}
},
"description": "An Assignment object represents the assignment associated with a Ticket."
},
"apiFilter": {
"type": "object",
"properties": {
"attribute": {
"type": "string",
"description": "Name of the ticket attribute this Filter operates on."
},
"max": {
"type": "number",
"format": "double",
"description": "Maximum value. Defaults to positive infinity (any value above minv)."
},
"min": {
"type": "number",
"format": "double",
"description": "Minimum value. Defaults to 0."
}
},
"description": "A hard filter used to query a subset of Tickets meeting the filtering\ncriteria."
},
"apiPool": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "A developer-chosen human-readable name for this Pool."
},
"filter": {
"type": "array",
"items": {
"$ref": "#/definitions/apiFilter"
},
"description": "Set of Filters indicating the filtering criteria. Selected players must\nmatch every Filter."
}
}
},
"apiQueryTicketsRequest": {
"type": "object",
"properties": {
"pool": {
"$ref": "#/definitions/apiPool",
"description": "The Pool representing the set of Filters to be queried."
}
}
},
"apiQueryTicketsResponse": {
"type": "object",
"properties": {
"ticket": {
"type": "array",
"items": {
"$ref": "#/definitions/apiTicket"
},
"description": "The Tickets that meet the Filter criteria requested by the Pool."
}
}
},
"apiTicket": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The Ticket ID generated by Open Match."
},
"properties": {
"$ref": "#/definitions/protobufStruct",
"description": "Properties contains custom info about the ticket. Top level values can be\nused in indexing and filtering to find tickets."
},
"assignment": {
"$ref": "#/definitions/apiAssignment",
"description": "Assignment associated with the Ticket."
}
},
"description": "A Ticket is a basic matchmaking entity in Open Match. In order to enter\nmatchmaking using Open Match, the client should generate a Ticket, passing in\nthe properties to be associated with this Ticket. Open Match will generate an\nID for a Ticket during creation. A Ticket could be used to represent an\nindividual 'Player' or a 'Group' of players. Open Match will not interpret\nwhat the Ticket represents but just treat it as a matchmaking unit with a set\nof properties. Open Match stores the Ticket in state storage and enables an\nAssignment to be associated with this Ticket."
},
"protobufAny": {
"type": "object",
"properties": {
"type_url": {
"type": "string",
"description": "A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. This string must contain at least\none \"/\" character. The last segment of the URL's path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading \".\" is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics."
},
"value": {
"type": "string",
"format": "byte",
"description": "Must be a valid serialized protocol buffer of the above specified type."
}
},
"description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\n Example 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := ptypes.MarshalAny(foo)\n ...\n foo := \u0026pb.Foo{}\n if err := ptypes.UnmarshalAny(any, foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }"
},
"protobufListValue": {
"type": "object",
"properties": {
"values": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufValue"
},
"description": "Repeated field of dynamically typed values."
}
},
"description": "`ListValue` is a wrapper around a repeated field of values.\n\nThe JSON representation for `ListValue` is JSON array."
},
"protobufNullValue": {
"type": "string",
"enum": [
"NULL_VALUE"
],
"default": "NULL_VALUE",
"description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value."
},
"protobufStruct": {
"type": "object",
"properties": {
"fields": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/protobufValue"
},
"description": "Unordered map of dynamically typed values."
}
},
"description": "`Struct` represents a structured data value, consisting of fields\nwhich map to dynamically typed values. In some languages, `Struct`\nmight be supported by a native representation. For example, in\nscripting languages like JS a struct is represented as an\nobject. The details of that representation are described together\nwith the proto support for the language.\n\nThe JSON representation for `Struct` is JSON object."
},
"protobufValue": {
"type": "object",
"properties": {
"null_value": {
"$ref": "#/definitions/protobufNullValue",
"description": "Represents a null value."
},
"number_value": {
"type": "number",
"format": "double",
"description": "Represents a double value."
},
"string_value": {
"type": "string",
"description": "Represents a string value."
},
"bool_value": {
"type": "boolean",
"format": "boolean",
"description": "Represents a boolean value."
},
"struct_value": {
"$ref": "#/definitions/protobufStruct",
"description": "Represents a structured value."
},
"list_value": {
"$ref": "#/definitions/protobufListValue",
"description": "Represents a repeated `Value`."
}
},
"description": "`Value` represents a dynamically typed value which can be either\nnull, a number, a string, a boolean, a recursive struct value, or a\nlist of values. A producer of value is expected to set one of that\nvariants, absence of any variant indicates an error.\n\nThe JSON representation for `Value` is JSON value."
},
"runtimeStreamError": {
"type": "object",
"properties": {
"grpc_code": {
"type": "integer",
"format": "int32"
},
"http_code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"http_status": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
},
"x-stream-definitions": {
"apiQueryTicketsResponse": {
"type": "object",
"properties": {
"result": {
"$ref": "#/definitions/apiQueryTicketsResponse"
},
"error": {
"$ref": "#/definitions/runtimeStreamError"
}
},
"title": "Stream result of apiQueryTicketsResponse"
}
},
"externalDocs": {
"description": "Open Match Documentation",
"url": "https://open-match.dev/site/docs/"
}
}

View File

@ -1,27 +0,0 @@
## REST compatibility
Follow the guidelines at https://cloud.google.com/endpoints/docs/grpc/transcoding to keep the gRPC service definitions friendly to REST transcoding. An excerpt:
"Transcoding involves mapping HTTP/JSON requests and their parameters to gRPC methods and their parameters and return types (we'll look at exactly how you do this in the following sections). Because of this, while it's possible to map an HTTP/JSON request to any arbitrary API method, it's simplest and most intuitive to do so if the gRPC API itself is structured in a resource-oriented way, just like a traditional HTTP REST API. In other words, the API service should be designed so that it uses a small number of standard methods (corresponding to HTTP verbs like GET, PUT, and so on) that operate on the service's resources (and collections of resources, which are themselves a type of resource). These standard methods are List, Get, Create, Update, and Delete."
It is for these reasons we don't have gRPC calls that support bi-directional streaming in Open Match.
## REST API Usage
Open Match gateway proxy transcodes any REST calls to its underlying gRPC service. Follow the [examples](https://cloud.google.com/endpoints/docs/grpc-service-config/reference/rpc/google.api#httprule) for further details.
A typical REST call to Open Match backend's `CreateAssignments` service via HTTP POST request
```
/v1/backend/assignments/123? \
assignment.rosters.name=foo&assignment.rosters.players.id=1&assignment.rosters.players.id=2
```
is equivalent to
```go
CreateAssignmentsRequest(
Assignments(
name: '123',
rosters: [
Roster(name: 'foo', [Player(id: 1), Player(id: 2)])
]
)
)
```

View File

@ -1,150 +0,0 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = 'proto3';
package api;
option go_package = "internal/pb";
// The protobuf messages sent in the gRPC calls are defined 'messages.proto'.
import 'api/protobuf-spec/messages.proto';
import 'google/api/annotations.proto';
message MmfConfig {
enum Type {
GRPC = 0;
REST = 1; // REST support will be added in future.
}
string name = 1; // Developer-chosen, human-readable string. (Optional)
string host = 2; // Host or DNS name for service providing this MMF. Must be resolve-able by the backend API.
int32 port = 3; // Port number for service providing this MMF.
Type type = 4; // Type of MMF call
}
message CreateMatchRequest {
messages.MatchObject match = 1;
MmfConfig mmfcfg = 2;
}
message CreateMatchResponse {
messages.MatchObject match = 1;
}
message ListMatchesRequest {
messages.MatchObject match = 1;
MmfConfig mmfcfg = 2;
}
message ListMatchesResponse {
messages.MatchObject match = 1;
}
message DeleteMatchRequest {
messages.MatchObject match = 1;
}
message DeleteMatchResponse {
}
message CreateAssignmentsRequest {
messages.Assignments assignment = 1;
}
message CreateAssignmentsResponse {
}
message DeleteAssignmentsRequest {
messages.Roster roster = 1;
}
message DeleteAssignmentsResponse {
}
service Backend {
// Calls to ask the matchmaker to run a matchmaking function.
// Run MMF once. Return a matchobject that fits this profile.
// INPUT: MatchObject message with these fields populated:
// - id
// - properties
// - [optional] roster, any fields you fill are available to your MMF.
// - [optional] pools, any fields you fill are available to your MMF.
// OUTPUT: MatchObject message with these fields populated:
// - id
// - properties
// - error. Empty if no error was encountered
// - rosters, if you choose to fill them in your MMF. (Recommended)
// - pools, if you used the MMLogicAPI in your MMF. (Recommended, and provides stats)
rpc CreateMatch(CreateMatchRequest) returns (CreateMatchResponse) {
option (google.api.http) = {
put: "/v1/backend/matches"
body: "*"
};
}
// Continually run MMF and stream MatchObjects that fit this profile until
// the backend client closes the connection. Same inputs/outputs as CreateMatch.
rpc ListMatches(ListMatchesRequest) returns (stream ListMatchesResponse) {
option (google.api.http).get = "/v1/backend/matches/{match.id}/{match.properties}";
}
// Delete a MatchObject from state storage manually. (MatchObjects in state
// storage will also automatically expire after a while, defined in the config)
// INPUT: MatchObject message with the 'id' field populated.
// (All other fields are ignored.)
rpc DeleteMatch(DeleteMatchRequest) returns (DeleteMatchResponse) {
option (google.api.http) = {
delete: "/v1/backend/matches"
body: "*"
additional_bindings {
delete: "/v1/backend/matches/{match.id}"
}
};
}
// Calls for communication of connection info to players.
// Write the connection info for the list of players in the
// Assignments.messages.Rosters to state storage. The Frontend API is
// responsible for sending anything sent here to the game clients.
// Sending a player to this function kicks off a process that removes
// the player from future matchmaking functions by adding them to the
// 'deindexed' player list and then deleting their player ID from state storage
// indexes.
// INPUT: Assignments message with these fields populated:
// - assignment, anything you write to this string is sent to Frontend API
// - rosters. You can send any number of rosters, containing any number of
// player messages. All players from all rosters will be sent the assignment.
// The only field in the Roster's Player messages used by CreateAssignments is
// the id field. All other fields in the Player messages are silently ignored.
rpc CreateAssignments(CreateAssignmentsRequest) returns (CreateAssignmentsResponse) {
option (google.api.http)= {
put: "/v1/backend/assignments"
body: "*"
};
}
// Remove DGS connection info from state storage for players.
// INPUT: Roster message with the 'players' field populated.
// The only field in the Roster's Player messages used by
// DeleteAssignments is the 'id' field. All others are silently ignored. If
// you need to delete multiple rosters, make multiple calls.
rpc DeleteAssignments(DeleteAssignmentsRequest) returns (DeleteAssignmentsResponse) {
option (google.api.http) = {
delete: "/v1/backend/assignments"
body: "*"
additional_bindings {
delete: "/v1/backend/assignments"
}
};
}
}

View File

@ -1,535 +0,0 @@
{
"swagger": "2.0",
"info": {
"title": "api/protobuf-spec/backend.proto",
"version": "version not set"
},
"schemes": [
"http",
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/backend/assignments": {
"delete": {
"summary": "Remove DGS connection info from state storage for players.\nINPUT: Roster message with the 'players' field populated.\n The only field in the Roster's Player messages used by\n DeleteAssignments is the 'id' field. All others are silently ignored. If\n you need to delete multiple rosters, make multiple calls.",
"operationId": "DeleteAssignments2",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiDeleteAssignmentsResponse"
}
}
},
"parameters": [
{
"name": "roster.name",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"Backend"
]
},
"put": {
"summary": "Write the connection info for the list of players in the\nAssignments.messages.Rosters to state storage. The Frontend API is\nresponsible for sending anything sent here to the game clients.\nSending a player to this function kicks off a process that removes\nthe player from future matchmaking functions by adding them to the\n'deindexed' player list and then deleting their player ID from state storage\nindexes.\nINPUT: Assignments message with these fields populated:\n - assignment, anything you write to this string is sent to Frontend API\n - rosters. You can send any number of rosters, containing any number of\n player messages. All players from all rosters will be sent the assignment.\n The only field in the Roster's Player messages used by CreateAssignments is\n the id field. All other fields in the Player messages are silently ignored.",
"operationId": "CreateAssignments",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiCreateAssignmentsResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiCreateAssignmentsRequest"
}
}
],
"tags": [
"Backend"
]
}
},
"/v1/backend/matches": {
"delete": {
"summary": "Delete a MatchObject from state storage manually. (MatchObjects in state\nstorage will also automatically expire after a while, defined in the config)\nINPUT: MatchObject message with the 'id' field populated.\n(All other fields are ignored.)",
"operationId": "DeleteMatch",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiDeleteMatchResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiDeleteMatchRequest"
}
}
],
"tags": [
"Backend"
]
},
"put": {
"summary": "Run MMF once. Return a matchobject that fits this profile.\nINPUT: MatchObject message with these fields populated:\n - id\n - properties\n - [optional] roster, any fields you fill are available to your MMF.\n - [optional] pools, any fields you fill are available to your MMF.\nOUTPUT: MatchObject message with these fields populated:\n - id\n - properties\n - error. Empty if no error was encountered\n - rosters, if you choose to fill them in your MMF. (Recommended)\n - pools, if you used the MMLogicAPI in your MMF. (Recommended, and provides stats)",
"operationId": "CreateMatch",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiCreateMatchResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiCreateMatchRequest"
}
}
],
"tags": [
"Backend"
]
}
},
"/v1/backend/matches/{match.id}": {
"delete": {
"summary": "Delete a MatchObject from state storage manually. (MatchObjects in state\nstorage will also automatically expire after a while, defined in the config)\nINPUT: MatchObject message with the 'id' field populated.\n(All other fields are ignored.)",
"operationId": "DeleteMatch2",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiDeleteMatchResponse"
}
}
},
"parameters": [
{
"name": "match.id",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "match.properties",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "match.error",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "match.status",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"Backend"
]
}
},
"/v1/backend/matches/{match.id}/{match.properties}": {
"get": {
"summary": "Continually run MMF and stream MatchObjects that fit this profile until\nthe backend client closes the connection. Same inputs/outputs as CreateMatch.",
"operationId": "ListMatches",
"responses": {
"200": {
"description": "A successful response.(streaming responses)",
"schema": {
"$ref": "#/x-stream-definitions/apiListMatchesResponse"
}
}
},
"parameters": [
{
"name": "match.id",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "match.properties",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "match.error",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "match.status",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "mmfcfg.name",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "mmfcfg.host",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "mmfcfg.port",
"in": "query",
"required": false,
"type": "integer",
"format": "int32"
},
{
"name": "mmfcfg.type",
"in": "query",
"required": false,
"type": "string",
"enum": [
"GRPC",
"REST"
],
"default": "GRPC"
}
],
"tags": [
"Backend"
]
}
}
},
"definitions": {
"PlayerAttribute": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string",
"format": "int64"
}
}
},
"apiCreateAssignmentsRequest": {
"type": "object",
"properties": {
"assignment": {
"$ref": "#/definitions/messagesAssignments"
}
}
},
"apiCreateAssignmentsResponse": {
"type": "object"
},
"apiCreateMatchRequest": {
"type": "object",
"properties": {
"match": {
"$ref": "#/definitions/messagesMatchObject"
},
"mmfcfg": {
"$ref": "#/definitions/apiMmfConfig"
}
}
},
"apiCreateMatchResponse": {
"type": "object",
"properties": {
"match": {
"$ref": "#/definitions/messagesMatchObject"
}
}
},
"apiDeleteAssignmentsRequest": {
"type": "object",
"properties": {
"roster": {
"$ref": "#/definitions/messagesRoster"
}
}
},
"apiDeleteAssignmentsResponse": {
"type": "object"
},
"apiDeleteMatchRequest": {
"type": "object",
"properties": {
"match": {
"$ref": "#/definitions/messagesMatchObject"
}
}
},
"apiDeleteMatchResponse": {
"type": "object"
},
"apiListMatchesResponse": {
"type": "object",
"properties": {
"match": {
"$ref": "#/definitions/messagesMatchObject"
}
}
},
"apiMmfConfig": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"host": {
"type": "string"
},
"port": {
"type": "integer",
"format": "int32"
},
"type": {
"$ref": "#/definitions/apiMmfConfigType"
}
}
},
"apiMmfConfigType": {
"type": "string",
"enum": [
"GRPC",
"REST"
],
"default": "GRPC"
},
"messagesAssignments": {
"type": "object",
"properties": {
"rosters": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesRoster"
}
},
"assignment": {
"type": "string"
}
}
},
"messagesFilter": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"attribute": {
"type": "string"
},
"maxv": {
"type": "string",
"format": "int64"
},
"minv": {
"type": "string",
"format": "int64"
},
"stats": {
"$ref": "#/definitions/messagesStats"
}
},
"description": "A 'hard' filter to apply to the player pool."
},
"messagesMatchObject": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"properties": {
"type": "string"
},
"error": {
"type": "string"
},
"rosters": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesRoster"
}
},
"pools": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesPlayerPool"
}
},
"status": {
"type": "string"
}
},
"description": "Open Match's internal representation and wire protocol format for \"MatchObjects\".\nIn order to request a match using the Backend API, your backend code should generate\na new MatchObject with an ID and properties filled in (for more details about valid\nvalues for these fields, see the documentation). Open Match then sends the Match\nObject through to your matchmaking function, where you add players to 'rosters' and\nstore any schemaless data you wish in the 'properties' field. The MatchObject\nis then sent, populated, out through the Backend API to your backend code.\n\nMatchObjects contain a number of fields, but many gRPC calls that take a\nMatchObject as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesPlayer": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"properties": {
"type": "string"
},
"pool": {
"type": "string"
},
"attributes": {
"type": "array",
"items": {
"$ref": "#/definitions/PlayerAttribute"
}
},
"assignment": {
"type": "string"
},
"status": {
"type": "string"
},
"error": {
"type": "string"
}
},
"description": "Open Match's internal representation and wire protocol format for \"Players\".\nIn order to enter matchmaking using the Frontend API, your client code should generate\na consistent (same result for each client every time they launch) with an ID and\nproperties filled in (for more details about valid values for these fields,\nsee the documentation).\nPlayers contain a number of fields, but the gRPC calls that take a\nPlayer as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesPlayerPool": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"filters": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesFilter"
}
},
"roster": {
"$ref": "#/definitions/messagesRoster"
},
"stats": {
"$ref": "#/definitions/messagesStats"
}
},
"description": "PlayerPools are defined by a set of 'hard' filters, and can be filled in\nwith the players that match those filters.\n\nPlayerPools contain a number of fields, but many gRPC calls that take a\nPlayerPool as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesRoster": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"players": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesPlayer"
}
}
},
"description": "Data structure to hold a list of players in a match."
},
"messagesStats": {
"type": "object",
"properties": {
"count": {
"type": "string",
"format": "int64"
},
"elapsed": {
"type": "number",
"format": "double"
}
},
"title": "Holds statistics"
},
"protobufAny": {
"type": "object",
"properties": {
"type_url": {
"type": "string"
},
"value": {
"type": "string",
"format": "byte"
}
}
},
"runtimeStreamError": {
"type": "object",
"properties": {
"grpc_code": {
"type": "integer",
"format": "int32"
},
"http_code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"http_status": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
},
"x-stream-definitions": {
"apiListMatchesResponse": {
"type": "object",
"properties": {
"result": {
"$ref": "#/definitions/apiListMatchesResponse"
},
"error": {
"$ref": "#/definitions/runtimeStreamError"
}
},
"title": "Stream result of apiListMatchesResponse"
}
}
}

View File

@ -1,112 +0,0 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = 'proto3';
package api;
option go_package = "internal/pb";
import 'api/protobuf-spec/messages.proto';
import 'google/api/annotations.proto';
message CreatePlayerRequest {
messages.Player player = 1;
}
message CreatePlayerResponse {
}
message DeletePlayerRequest {
messages.Player player = 1;
}
message DeletePlayerResponse {
}
message GetUpdatesRequest {
messages.Player player = 1;
}
message GetUpdatesResponse {
messages.Player player = 1;
}
service Frontend {
// Call to start matchmaking for a player
// CreatePlayer will put the player in state storage, and then look
// through the 'properties' field for the attributes you have defined as
// indices your matchmaker config. If the attributes exist and are valid
// integers, they will be indexed.
// INPUT: Player message with these fields populated:
// - id
// - properties
// OUTPUT: Result message denoting success or failure (and an error if
// necessary)
rpc CreatePlayer(CreatePlayerRequest) returns (CreatePlayerResponse) {
option (google.api.http) = {
put: "/v1/frontend/players"
body: "*"
};
}
// Call to stop matchmaking for a player
// DeletePlayer removes the player from state storage by doing the
// following:
// 1) Delete player from configured indices. This effectively removes the
// player from matchmaking when using recommended MMF patterns.
// Everything after this is just cleanup to save stage storage space.
// 2) 'Lazily' delete the player's state storage record. This is kicked
// off in the background and may take some time to complete.
// 2) 'Lazily' delete the player's metadata indicies (like, the timestamp when
// they called CreatePlayer, and the last time the record was accessed). This
// is also kicked off in the background and may take some time to complete.
// INPUT: Player message with the 'id' field populated.
// OUTPUT: Result message denoting success or failure (and an error if
// necessary)
rpc DeletePlayer(DeletePlayerRequest) returns (DeletePlayerResponse) {
option (google.api.http).delete = "/v1/frontend/players/{player.id}";
}
// Calls to access matchmaking results for a player
// GetUpdates streams matchmaking results from Open Match for the
// provided player ID.
// INPUT: Player message with the 'id' field populated.
// OUTPUT: a stream of player objects with one or more of the following
// fields populated, if an update to that field is seen in state storage:
// - 'assignment': string that usually contains game server connection information.
// - 'status': string to communicate current matchmaking status to the client.
// - 'error': string to pass along error information to the client.
//
// During normal operation, the expectation is that the 'assignment' field
// will be updated by a Backend process calling the 'CreateAssignments' Backend API
// endpoint. 'Status' and 'Error' are free for developers to use as they see fit.
// Even if you had multiple players enter a matchmaking request as a group, the
// Backend API 'CreateAssignments' call will write the results to state
// storage separately under each player's ID. OM expects you to make all game
// clients 'GetUpdates' with their own ID from the Frontend API to get
// their results.
//
// NOTE: This call generates a small amount of load on the Frontend API and state
// storage while watching the player record for updates. You are expected
// to close the stream from your client after receiving your matchmaking
// results (or a reasonable timeout), or you will continue to
// generate load on OM until you do!
// NOTE: Just bear in mind that every update will send egress traffic from
// Open Match to game clients! Frugality is recommended.
rpc GetUpdates(GetUpdatesRequest) returns (stream GetUpdatesResponse) {
option (google.api.http).get = "/v1/frontend/players/{player.id}";
}
}

View File

@ -1,272 +0,0 @@
{
"swagger": "2.0",
"info": {
"title": "api/protobuf-spec/frontend.proto",
"version": "version not set"
},
"schemes": [
"http",
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/frontend/players": {
"put": {
"summary": "CreatePlayer will put the player in state storage, and then look\nthrough the 'properties' field for the attributes you have defined as\nindices your matchmaker config. If the attributes exist and are valid\nintegers, they will be indexed.\nINPUT: Player message with these fields populated:\n - id\n - properties\nOUTPUT: Result message denoting success or failure (and an error if\nnecessary)",
"operationId": "CreatePlayer",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiCreatePlayerResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiCreatePlayerRequest"
}
}
],
"tags": [
"Frontend"
]
}
},
"/v1/frontend/players/{player.id}": {
"get": {
"summary": "GetUpdates streams matchmaking results from Open Match for the\nprovided player ID.\nINPUT: Player message with the 'id' field populated.\nOUTPUT: a stream of player objects with one or more of the following\nfields populated, if an update to that field is seen in state storage:\n - 'assignment': string that usually contains game server connection information.\n - 'status': string to communicate current matchmaking status to the client.\n - 'error': string to pass along error information to the client.",
"description": "During normal operation, the expectation is that the 'assignment' field\nwill be updated by a Backend process calling the 'CreateAssignments' Backend API\nendpoint. 'Status' and 'Error' are free for developers to use as they see fit. \nEven if you had multiple players enter a matchmaking request as a group, the\nBackend API 'CreateAssignments' call will write the results to state\nstorage separately under each player's ID. OM expects you to make all game\nclients 'GetUpdates' with their own ID from the Frontend API to get\ntheir results.\n\nNOTE: This call generates a small amount of load on the Frontend API and state\n storage while watching the player record for updates. You are expected\n to close the stream from your client after receiving your matchmaking\n results (or a reasonable timeout), or you will continue to\n generate load on OM until you do!\nNOTE: Just bear in mind that every update will send egress traffic from\n Open Match to game clients! Frugality is recommended.",
"operationId": "GetUpdates",
"responses": {
"200": {
"description": "A successful response.(streaming responses)",
"schema": {
"$ref": "#/x-stream-definitions/apiGetUpdatesResponse"
}
}
},
"parameters": [
{
"name": "player.id",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "player.properties",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.pool",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.assignment",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.status",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.error",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"Frontend"
]
},
"delete": {
"summary": "DeletePlayer removes the player from state storage by doing the\nfollowing:\n 1) Delete player from configured indices. This effectively removes the\n player from matchmaking when using recommended MMF patterns.\n Everything after this is just cleanup to save stage storage space.\n 2) 'Lazily' delete the player's state storage record. This is kicked\n off in the background and may take some time to complete.\n 2) 'Lazily' delete the player's metadata indicies (like, the timestamp when \n they called CreatePlayer, and the last time the record was accessed). This \n is also kicked off in the background and may take some time to complete.\nINPUT: Player message with the 'id' field populated.\nOUTPUT: Result message denoting success or failure (and an error if\nnecessary)",
"operationId": "DeletePlayer",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiDeletePlayerResponse"
}
}
},
"parameters": [
{
"name": "player.id",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "player.properties",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.pool",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.assignment",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.status",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player.error",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"Frontend"
]
}
}
},
"definitions": {
"PlayerAttribute": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string",
"format": "int64"
}
}
},
"apiCreatePlayerRequest": {
"type": "object",
"properties": {
"player": {
"$ref": "#/definitions/messagesPlayer"
}
}
},
"apiCreatePlayerResponse": {
"type": "object"
},
"apiDeletePlayerResponse": {
"type": "object"
},
"apiGetUpdatesResponse": {
"type": "object",
"properties": {
"player": {
"$ref": "#/definitions/messagesPlayer"
}
}
},
"messagesPlayer": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"properties": {
"type": "string"
},
"pool": {
"type": "string"
},
"attributes": {
"type": "array",
"items": {
"$ref": "#/definitions/PlayerAttribute"
}
},
"assignment": {
"type": "string"
},
"status": {
"type": "string"
},
"error": {
"type": "string"
}
},
"description": "Open Match's internal representation and wire protocol format for \"Players\".\nIn order to enter matchmaking using the Frontend API, your client code should generate\na consistent (same result for each client every time they launch) with an ID and\nproperties filled in (for more details about valid values for these fields,\nsee the documentation).\nPlayers contain a number of fields, but the gRPC calls that take a\nPlayer as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"protobufAny": {
"type": "object",
"properties": {
"type_url": {
"type": "string"
},
"value": {
"type": "string",
"format": "byte"
}
}
},
"runtimeStreamError": {
"type": "object",
"properties": {
"grpc_code": {
"type": "integer",
"format": "int32"
},
"http_code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"http_status": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
},
"x-stream-definitions": {
"apiGetUpdatesResponse": {
"type": "object",
"properties": {
"result": {
"$ref": "#/definitions/apiGetUpdatesResponse"
},
"error": {
"$ref": "#/definitions/runtimeStreamError"
}
},
"title": "Stream result of apiGetUpdatesResponse"
}
}
}

View File

@ -1,48 +0,0 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = 'proto3';
package api;
option go_package = "internal/pb";
// The protobuf messages sent in the gRPC calls are defined 'messages.proto'.
import 'api/protobuf-spec/messages.proto';
import 'google/api/annotations.proto';
// Request message sent to the MMF.
message RunRequest {
string profile_id = 1; // Developer-chosen profile name, state storage key for the match object.
string proposal_id = 2; // The ID against which, the generated proposal should be stored.
string result_id = 3; // Final result ID. MMF needs to know this in case of errors where proposal generation can be shortcircuited.
messages.MatchObject match_object = 4; // The match object containing the details of the match to be generated.
string timestamp = 5;
}
message RunResponse {
}
// The MMF proto defines the API for running MMFs as long-lived, 'serving'
// functions inside of the kubernetes cluster.
service MatchFunction {
// The assumption is that there will be one service for each MMF that is
// being served. Build your MMF in the appropriate serving harness, deploy it
// to the K8s cluster with a unique service name, then connect to that service
// and call 'Run()' to execute the fuction.
rpc Run(RunRequest) returns (RunResponse) {
option (google.api.http) = {
put: "/v1/function"
body: "*"
};
}
}

View File

@ -1,217 +0,0 @@
{
"swagger": "2.0",
"info": {
"title": "api/protobuf-spec/matchfunction.proto",
"version": "version not set"
},
"schemes": [
"http",
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/function": {
"put": {
"summary": "The assumption is that there will be one service for each MMF that is\nbeing served. Build your MMF in the appropriate serving harness, deploy it\nto the K8s cluster with a unique service name, then connect to that service\nand call 'Run()' to execute the fuction.",
"operationId": "Run",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiRunResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiRunRequest"
}
}
],
"tags": [
"MatchFunction"
]
}
}
},
"definitions": {
"PlayerAttribute": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string",
"format": "int64"
}
}
},
"apiRunRequest": {
"type": "object",
"properties": {
"profile_id": {
"type": "string"
},
"proposal_id": {
"type": "string"
},
"result_id": {
"type": "string"
},
"match_object": {
"$ref": "#/definitions/messagesMatchObject"
},
"timestamp": {
"type": "string"
}
},
"description": "Request message sent to the MMF."
},
"apiRunResponse": {
"type": "object"
},
"messagesFilter": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"attribute": {
"type": "string"
},
"maxv": {
"type": "string",
"format": "int64"
},
"minv": {
"type": "string",
"format": "int64"
},
"stats": {
"$ref": "#/definitions/messagesStats"
}
},
"description": "A 'hard' filter to apply to the player pool."
},
"messagesMatchObject": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"properties": {
"type": "string"
},
"error": {
"type": "string"
},
"rosters": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesRoster"
}
},
"pools": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesPlayerPool"
}
},
"status": {
"type": "string"
}
},
"description": "Open Match's internal representation and wire protocol format for \"MatchObjects\".\nIn order to request a match using the Backend API, your backend code should generate\na new MatchObject with an ID and properties filled in (for more details about valid\nvalues for these fields, see the documentation). Open Match then sends the Match\nObject through to your matchmaking function, where you add players to 'rosters' and\nstore any schemaless data you wish in the 'properties' field. The MatchObject\nis then sent, populated, out through the Backend API to your backend code.\n\nMatchObjects contain a number of fields, but many gRPC calls that take a\nMatchObject as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesPlayer": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"properties": {
"type": "string"
},
"pool": {
"type": "string"
},
"attributes": {
"type": "array",
"items": {
"$ref": "#/definitions/PlayerAttribute"
}
},
"assignment": {
"type": "string"
},
"status": {
"type": "string"
},
"error": {
"type": "string"
}
},
"description": "Open Match's internal representation and wire protocol format for \"Players\".\nIn order to enter matchmaking using the Frontend API, your client code should generate\na consistent (same result for each client every time they launch) with an ID and\nproperties filled in (for more details about valid values for these fields,\nsee the documentation).\nPlayers contain a number of fields, but the gRPC calls that take a\nPlayer as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesPlayerPool": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"filters": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesFilter"
}
},
"roster": {
"$ref": "#/definitions/messagesRoster"
},
"stats": {
"$ref": "#/definitions/messagesStats"
}
},
"description": "PlayerPools are defined by a set of 'hard' filters, and can be filled in\nwith the players that match those filters.\n\nPlayerPools contain a number of fields, but many gRPC calls that take a\nPlayerPool as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesRoster": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"players": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesPlayer"
}
}
},
"description": "Data structure to hold a list of players in a match."
},
"messagesStats": {
"type": "object",
"properties": {
"count": {
"type": "string",
"format": "int64"
},
"elapsed": {
"type": "number",
"format": "double"
}
},
"title": "Holds statistics"
}
}
}

View File

@ -1,102 +0,0 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = 'proto3';
package messages;
option go_package = "internal/pb";
// Open Match's internal representation and wire protocol format for "MatchObjects".
// In order to request a match using the Backend API, your backend code should generate
// a new MatchObject with an ID and properties filled in (for more details about valid
// values for these fields, see the documentation). Open Match then sends the Match
// Object through to your matchmaking function, where you add players to 'rosters' and
// store any schemaless data you wish in the 'properties' field. The MatchObject
// is then sent, populated, out through the Backend API to your backend code.
//
// MatchObjects contain a number of fields, but many gRPC calls that take a
// MatchObject as input only require a few of them to be filled in. Check the
// gRPC function in question for more details.
message MatchObject {
string id = 1; // By convention, an Xid
string properties = 2; // By convention, a JSON-encoded string
string error = 3; // Last error encountered.
repeated Roster rosters = 4; // Rosters of players.
repeated PlayerPool pools = 5; // 'Hard' filters, and the players who match them.
string status = 6; // Resulting status of the match function
}
// Data structure to hold a list of players in a match.
message Roster {
string name = 1; // Arbitrary developer-chosen, human-readable string. By convention, set to team name.
repeated Player players = 2; // Player profiles on this roster.
}
// A 'hard' filter to apply to the player pool.
message Filter {
string name = 1; // Arbitrary developer-chosen, human-readable name of this filter. Appears in logs and metrics.
string attribute = 2; // Name of the player attribute this filter operates on.
int64 maxv = 3; // Maximum value. Defaults to positive infinity (any value above minv).
int64 minv = 4; // Minimum value. Defaults to 0.
Stats stats = 5; // Statistics for the last time the filter was applied.
}
// Holds statistics
message Stats {
int64 count = 1; // Number of results.
double elapsed = 2; // How long it took to get the results.
}
// PlayerPools are defined by a set of 'hard' filters, and can be filled in
// with the players that match those filters.
//
// PlayerPools contain a number of fields, but many gRPC calls that take a
// PlayerPool as input only require a few of them to be filled in. Check the
// gRPC function in question for more details.
message PlayerPool {
string name = 1; // Arbitrary developer-chosen, human-readable string.
repeated Filter filters = 2; // Filters are logical AND-ed (a player must match every filter).
Roster roster = 3; // Roster of players that match all filters.
Stats stats = 4; // Statisticss for the last time this Pool was retrieved from state storage.
}
// Open Match's internal representation and wire protocol format for "Players".
// In order to enter matchmaking using the Frontend API, your client code should generate
// a consistent (same result for each client every time they launch) with an ID and
// properties filled in (for more details about valid values for these fields,
// see the documentation).
// Players contain a number of fields, but the gRPC calls that take a
// Player as input only require a few of them to be filled in. Check the
// gRPC function in question for more details.
message Player {
message Attribute {
string name = 1; // Name should match a Filter.attribute field.
int64 value = 2;
}
string id = 1; // By convention, an Xid
string properties = 2; // By convention, a JSON-encoded string
string pool = 3; // Optionally used to specify the PlayerPool in which to find a player.
repeated Attribute attributes = 4; // Attributes of this player.
string assignment = 5; // By convention, ip:port of a DGS to connect to
string status = 6; // Arbitrary developer-chosen string.
string error = 7; // Arbitrary developer-chosen string.
}
// IlInput is an empty message reserved for future use.
message IlInput {
}
message Assignments {
repeated Roster rosters = 1;
string assignment = 10;
}

View File

@ -1,137 +0,0 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = 'proto3';
package api;
option go_package = "internal/pb";
// The protobuf messages sent in the gRPC calls are defined 'messages.proto'.
import 'api/protobuf-spec/messages.proto';
import 'google/api/annotations.proto';
message GetProfileRequest {
messages.MatchObject match = 1;
}
message GetProfileResponse {
messages.MatchObject match = 1;
}
message CreateProposalRequest {
messages.MatchObject match = 1;
}
message CreateProposalResponse {
}
message GetPlayerPoolRequest {
messages.PlayerPool player_pool = 1;
}
message GetPlayerPoolResponse {
messages.PlayerPool player_pool = 1;
}
message GetAllIgnoredPlayersRequest {
messages.IlInput ignore_player = 1;
}
message GetAllIgnoredPlayersResponse {
messages.Roster roster = 1;
}
message ListIgnoredPlayersRequest {
messages.IlInput ignore_player = 1;
}
message ListIgnoredPlayersResponse {
messages.Roster roster = 1;
}
// The MMLogic API provides utility functions for common MMF functionality, such
// as retreiving profiles and players from state storage, writing results to state storage,
// and exposing metrics and statistics.
service MmLogic {
// Profile and match object functions
// Send GetProfile a match object with the ID field populated, it will return a
// 'filled' one.
// Note: filters are assumed to have been checked for validity by the
// backendapi when accepting a profile
rpc GetProfile(GetProfileRequest) returns (GetProfileResponse) {
option (google.api.http).get = "/v1/logic/match-profiles/{match.id}";
}
// CreateProposal is called by MMFs that wish to write their results to
// a proposed MatchObject, that can be sent out the Backend API once it has
// been approved (by default, by the evaluator process).
// - adds all players in all Rosters to the proposed player ignore list
// - writes the proposed match to the provided key
// - adds that key to the list of proposals to be considered
// INPUT:
// * TO RETURN A MATCHOBJECT AFTER A SUCCESSFUL MMF RUN
// To create a match MatchObject message with these fields populated:
// - id, set to the value of the MMF_PROPOSAL_ID env var
// - properties
// - error. You must explicitly set this to an empty string if your MMF
// - roster, with the playerIDs filled in the 'players' repeated field.
// - [optional] pools, set to the output from the 'GetPlayerPools' call,
// will populate the pools with stats about how many players the filters
// matched and how long the filters took to run, which will be sent out
// the backend api along with your match results.
// was successful.
// * TO RETURN AN ERROR
// To report a failure or error, send a MatchObject message with these
// these fields populated:
// - id, set to the value of the MMF_ERROR_ID env var.
// - error, set to a string value describing the error your MMF encountered.
// - [optional] properties, anything you put here is returned to the
// backend along with your error.
// - [optional] rosters, anything you put here is returned to the
// backend along with your error.
// - [optional] pools, set to the output from the 'GetPlayerPools' call,
// will populate the pools with stats about how many players the filters
// matched and how long the filters took to run, which will be sent out
// the backend api along with your match results.
// OUTPUT: a Result message with a boolean success value and an error string
// if an error was encountered
rpc CreateProposal(CreateProposalRequest) returns (CreateProposalResponse) {
option (google.api.http) = {
put: "/v1/logic/match-proposals"
body: "*"
};
}
// Player listing and filtering functions
//
// RetrievePlayerPool gets the list of players that match every Filter in the
// PlayerPool, .excluding players in any configured ignore lists. It
// combines the results, and returns the resulting player pool.
rpc GetPlayerPool(GetPlayerPoolRequest) returns (stream GetPlayerPoolResponse) {
option (google.api.http).get = "/v1/logic/player-pools/{player_pool.name}";
}
// Ignore List functions
//
// IlInput is an empty message reserved for future use.
rpc GetAllIgnoredPlayers(GetAllIgnoredPlayersRequest) returns (GetAllIgnoredPlayersResponse) {}
// ListIgnoredPlayers retrieves players from the ignore list specified in the
// config file under 'ignoreLists.proposed.name'.
rpc ListIgnoredPlayers(ListIgnoredPlayersRequest) returns (ListIgnoredPlayersResponse) {}
// NYI
// UpdateMetrics sends stats about the MMF run to export to a metrics aggregation tool
// like Prometheus or StackDriver.
// rpc UpdateMetrics(messages.NYI) returns (messages.Results) {}
}

View File

@ -1,380 +0,0 @@
{
"swagger": "2.0",
"info": {
"title": "api/protobuf-spec/mmlogic.proto",
"version": "version not set"
},
"schemes": [
"http",
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/logic/match-profiles/{match.id}": {
"get": {
"summary": "Send GetProfile a match object with the ID field populated, it will return a\n 'filled' one.\n Note: filters are assumed to have been checked for validity by the\n backendapi when accepting a profile",
"operationId": "GetProfile",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiGetProfileResponse"
}
}
},
"parameters": [
{
"name": "match.id",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "match.properties",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "match.error",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "match.status",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"MmLogic"
]
}
},
"/v1/logic/match-proposals": {
"put": {
"summary": "CreateProposal is called by MMFs that wish to write their results to\na proposed MatchObject, that can be sent out the Backend API once it has\nbeen approved (by default, by the evaluator process).\n - adds all players in all Rosters to the proposed player ignore list\n - writes the proposed match to the provided key\n - adds that key to the list of proposals to be considered\nINPUT: \n * TO RETURN A MATCHOBJECT AFTER A SUCCESSFUL MMF RUN\n To create a match MatchObject message with these fields populated:\n - id, set to the value of the MMF_PROPOSAL_ID env var\n - properties\n - error. You must explicitly set this to an empty string if your MMF\n - roster, with the playerIDs filled in the 'players' repeated field. \n - [optional] pools, set to the output from the 'GetPlayerPools' call,\n will populate the pools with stats about how many players the filters\n matched and how long the filters took to run, which will be sent out\n the backend api along with your match results.\n was successful.\n * TO RETURN AN ERROR \n To report a failure or error, send a MatchObject message with these\n these fields populated:\n - id, set to the value of the MMF_ERROR_ID env var. \n - error, set to a string value describing the error your MMF encountered.\n - [optional] properties, anything you put here is returned to the\n backend along with your error.\n - [optional] rosters, anything you put here is returned to the\n backend along with your error.\n - [optional] pools, set to the output from the 'GetPlayerPools' call,\n will populate the pools with stats about how many players the filters\n matched and how long the filters took to run, which will be sent out\n the backend api along with your match results.\nOUTPUT: a Result message with a boolean success value and an error string\nif an error was encountered",
"operationId": "CreateProposal",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/apiCreateProposalResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/apiCreateProposalRequest"
}
}
],
"tags": [
"MmLogic"
]
}
},
"/v1/logic/player-pools/{player_pool.name}": {
"get": {
"summary": "Player listing and filtering functions",
"description": "RetrievePlayerPool gets the list of players that match every Filter in the\nPlayerPool, .excluding players in any configured ignore lists. It\ncombines the results, and returns the resulting player pool.",
"operationId": "GetPlayerPool",
"responses": {
"200": {
"description": "A successful response.(streaming responses)",
"schema": {
"$ref": "#/x-stream-definitions/apiGetPlayerPoolResponse"
}
}
},
"parameters": [
{
"name": "player_pool.name",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "player_pool.roster.name",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "player_pool.stats.count",
"in": "query",
"required": false,
"type": "string",
"format": "int64"
},
{
"name": "player_pool.stats.elapsed",
"in": "query",
"required": false,
"type": "number",
"format": "double"
}
],
"tags": [
"MmLogic"
]
}
}
},
"definitions": {
"PlayerAttribute": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string",
"format": "int64"
}
}
},
"apiCreateProposalRequest": {
"type": "object",
"properties": {
"match": {
"$ref": "#/definitions/messagesMatchObject"
}
}
},
"apiCreateProposalResponse": {
"type": "object"
},
"apiGetAllIgnoredPlayersResponse": {
"type": "object",
"properties": {
"roster": {
"$ref": "#/definitions/messagesRoster"
}
}
},
"apiGetPlayerPoolResponse": {
"type": "object",
"properties": {
"player_pool": {
"$ref": "#/definitions/messagesPlayerPool"
}
}
},
"apiGetProfileResponse": {
"type": "object",
"properties": {
"match": {
"$ref": "#/definitions/messagesMatchObject"
}
}
},
"apiListIgnoredPlayersResponse": {
"type": "object",
"properties": {
"roster": {
"$ref": "#/definitions/messagesRoster"
}
}
},
"messagesFilter": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"attribute": {
"type": "string"
},
"maxv": {
"type": "string",
"format": "int64"
},
"minv": {
"type": "string",
"format": "int64"
},
"stats": {
"$ref": "#/definitions/messagesStats"
}
},
"description": "A 'hard' filter to apply to the player pool."
},
"messagesIlInput": {
"type": "object",
"description": "IlInput is an empty message reserved for future use."
},
"messagesMatchObject": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"properties": {
"type": "string"
},
"error": {
"type": "string"
},
"rosters": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesRoster"
}
},
"pools": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesPlayerPool"
}
},
"status": {
"type": "string"
}
},
"description": "Open Match's internal representation and wire protocol format for \"MatchObjects\".\nIn order to request a match using the Backend API, your backend code should generate\na new MatchObject with an ID and properties filled in (for more details about valid\nvalues for these fields, see the documentation). Open Match then sends the Match\nObject through to your matchmaking function, where you add players to 'rosters' and\nstore any schemaless data you wish in the 'properties' field. The MatchObject\nis then sent, populated, out through the Backend API to your backend code.\n\nMatchObjects contain a number of fields, but many gRPC calls that take a\nMatchObject as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesPlayer": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"properties": {
"type": "string"
},
"pool": {
"type": "string"
},
"attributes": {
"type": "array",
"items": {
"$ref": "#/definitions/PlayerAttribute"
}
},
"assignment": {
"type": "string"
},
"status": {
"type": "string"
},
"error": {
"type": "string"
}
},
"description": "Open Match's internal representation and wire protocol format for \"Players\".\nIn order to enter matchmaking using the Frontend API, your client code should generate\na consistent (same result for each client every time they launch) with an ID and\nproperties filled in (for more details about valid values for these fields,\nsee the documentation).\nPlayers contain a number of fields, but the gRPC calls that take a\nPlayer as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesPlayerPool": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"filters": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesFilter"
}
},
"roster": {
"$ref": "#/definitions/messagesRoster"
},
"stats": {
"$ref": "#/definitions/messagesStats"
}
},
"description": "PlayerPools are defined by a set of 'hard' filters, and can be filled in\nwith the players that match those filters.\n\nPlayerPools contain a number of fields, but many gRPC calls that take a\nPlayerPool as input only require a few of them to be filled in. Check the\ngRPC function in question for more details."
},
"messagesRoster": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"players": {
"type": "array",
"items": {
"$ref": "#/definitions/messagesPlayer"
}
}
},
"description": "Data structure to hold a list of players in a match."
},
"messagesStats": {
"type": "object",
"properties": {
"count": {
"type": "string",
"format": "int64"
},
"elapsed": {
"type": "number",
"format": "double"
}
},
"title": "Holds statistics"
},
"protobufAny": {
"type": "object",
"properties": {
"type_url": {
"type": "string"
},
"value": {
"type": "string",
"format": "byte"
}
}
},
"runtimeStreamError": {
"type": "object",
"properties": {
"grpc_code": {
"type": "integer",
"format": "int32"
},
"http_code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"http_status": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
},
"x-stream-definitions": {
"apiGetPlayerPoolResponse": {
"type": "object",
"properties": {
"result": {
"$ref": "#/definitions/apiGetPlayerPoolResponse"
},
"error": {
"$ref": "#/definitions/runtimeStreamError"
}
},
"title": "Stream result of apiGetPlayerPoolResponse"
}
}
}

View File

@ -1,4 +1,4 @@
# Copyright 2019 Google Inc. All Rights Reserved.
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -47,12 +47,16 @@
# https://github.com/GoogleCloudPlatform/cloud-builders/tree/master/go
steps:
# Blocked by https://github.com/GoogleContainerTools/kaniko/issues/477
- id: 'Docker Image: open-match-build'
name: gcr.io/kaniko-project/executor
args: ['--destination=gcr.io/$PROJECT_ID/open-match-build', '--cache=true', '--cache-ttl=6h', '--dockerfile=Dockerfile.ci', '.']
args: ['--destination=gcr.io/$PROJECT_ID/open-match-build', '--cache=true', '--cache-ttl=48h', '--dockerfile=Dockerfile.ci', '.']
waitFor: ['-']
- id: 'Test: Markdown'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'md-test']
waitFor: ['Docker Image: open-match-build']
- id: 'Build: Clean'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'clean']
@ -74,9 +78,9 @@ steps:
path: '/go'
waitFor: ['Setup: Download Dependencies']
- id: 'Build: Protocol Buffers'
- id: 'Build: Assets'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'all-protos']
args: ['make', 'all-protos', 'tls-certs', '-j8']
volumes:
- name: 'go-vol'
path: '/go'
@ -88,22 +92,18 @@ steps:
volumes:
- name: 'go-vol'
path: '/go'
waitFor: ['Build: Protocol Buffers']
waitFor: ['Build: Assets']
- id: 'Test: Core'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'GOPROXY=off', 'test-in-ci']
args: ['make', 'GOPROXY=off', 'ci-test']
volumes:
- name: 'go-vol'
path: '/go'
waitFor: ['Build: Protocol Buffers']
waitFor: ['Build: Assets']
- id: 'Build: Docker Images'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'VERSION_SUFFIX=$SHORT_SHA', 'build-images', '-j8']
waitFor: ['Build: Protocol Buffers']
- id: 'Build: Push Images'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', 'VERSION_SUFFIX=$SHORT_SHA', 'push-images', '-j8']
waitFor: ['Build: Docker Images']
waitFor: ['Build: Assets']
- id: 'Build: Deployment Configs'
name: 'gcr.io/$PROJECT_ID/open-match-build'
@ -116,7 +116,7 @@ steps:
volumes:
- name: 'go-vol'
path: '/go'
waitFor: ['Build: Protocol Buffers', 'Build: Deployment Configs']
waitFor: ['Build: Assets', 'Build: Deployment Configs']
- id: 'Build: Website'
name: 'gcr.io/$PROJECT_ID/open-match-build'
@ -130,7 +130,7 @@ steps:
- id: 'Deploy: Website'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', '_GCB_POST_SUBMIT=${_GCB_POST_SUBMIT}', VERSION_SUFFIX=$SHORT_SHA', 'BRANCH_NAME=$BRANCH_NAME', 'ci-deploy-dev-site']
args: ['make', '_GCB_POST_SUBMIT=${_GCB_POST_SUBMIT}', '_GCB_LATEST_VERSION=${_GCB_LATEST_VERSION}', VERSION_SUFFIX=$SHORT_SHA', 'BRANCH_NAME=$BRANCH_NAME', 'ci-deploy-site']
waitFor: ['Test: Website', 'Build: Binaries']
volumes:
- name: 'go-vol'
@ -138,70 +138,42 @@ steps:
- id: 'Deploy: Deployment Configs'
name: 'gcr.io/$PROJECT_ID/open-match-build'
args: ['make', '_GCB_POST_SUBMIT=${_GCB_POST_SUBMIT}', VERSION_SUFFIX=$SHORT_SHA', 'BRANCH_NAME=$BRANCH_NAME', 'ci-deploy-artifacts']
args: ['make', '_GCB_POST_SUBMIT=${_GCB_POST_SUBMIT}', '_GCB_LATEST_VERSION=${_GCB_LATEST_VERSION}', VERSION_SUFFIX=$SHORT_SHA', 'BRANCH_NAME=$BRANCH_NAME', 'ci-deploy-artifacts']
waitFor: ['Lint: Format, Vet, Charts', 'Build: Binaries']
volumes:
- name: 'go-vol'
path: '/go'
#- id: 'Deploy: Create Cluster'
# name: 'gcr.io/$PROJECT_ID/open-match-build'
# args: ['make', 'create-gke-cluster', 'push-helm']
# waitFor: ['Build: Docker Images']
#- id: 'Deploy: Install Charts'
# name: 'gcr.io/$PROJECT_ID/open-match-build'
# args: ['make', 'sleep-10', 'install-chart', 'install-example-chart']
# waitFor: ['Deploy: Create Cluster']
#- id: 'Deploy: Teardown Cluster'
# name: 'gcr.io/$PROJECT_ID/open-match-build'
# args: ['make', 'sleep-10', 'delete-gke-cluster']
# waitFor: ['Deploy: Install Charts']
artifacts:
objects:
location: gs://open-match-build-artifacts/output/
paths:
- cmd/backend/backend
- cmd/frontend/frontend
- cmd/mmlogic/mmlogic
- cmd/evaluator/evaluator
- cmd/minimatch/minimatch
- cmd/backendapi/backendapi
- cmd/frontendapi/frontendapi
- cmd/mmlogicapi/mmlogicapi
- examples/functions/golang/grpc-serving/grpc-serving
- examples/evaluators/golang/serving/serving
- examples/backendclient/backendclient
- test/cmd/clientloadgen/clientloadgen
- test/cmd/frontendclient/frontendclient
- install/yaml/install.yaml
- install/yaml/install-example.yaml
- install/yaml/install-demo.yaml
- install/yaml/01-redis-chart.yaml
- install/yaml/02-open-match.yaml
- install/yaml/03-prometheus-chart.yaml
- install/yaml/04-grafana-chart.yaml
- install/yaml/05-jaeger-chart.yaml
- examples/functions/golang/simple/simple
images:
- 'gcr.io/$PROJECT_ID/openmatch-backend:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-frontend:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-mmlogic:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-evaluator:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-minimatch:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-backendapi:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-frontendapi:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-mmlogicapi:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-evaluator-serving:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-mmf-go-grpc-serving-simple:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-backendclient:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-clientloadgen:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-frontendclient:${_OM_VERSION}-${SHORT_SHA}'
- 'gcr.io/$PROJECT_ID/openmatch-mmf-go-simple:${_OM_VERSION}-${SHORT_SHA}'
substitutions:
_OM_VERSION: "0.5.0-rc1"
_OM_VERSION: "0.0.0-dev"
_GCB_POST_SUBMIT: "0"
_GCB_LATEST_VERSION: "undefined"
logsBucket: 'gs://open-match-build-logs/'
options:
sourceProvenanceHash: ['SHA256']
machineType: 'N1_HIGHCPU_8'
# TODO: The build is slow because we don't vendor. go get takes a very long time.
# Also we are rebuilding a lot of code unnecessarily. This should improve once
# we have new hermetic and reproducible Dockerfiles.
machineType: 'N1_HIGHCPU_32'
timeout: 1200s
# TODO Build Steps
# config/matchmaker_config.yaml: Lint this file so it's verified as a valid YAML file.
# examples/profiles/*.json: Verify valid JSON files.
#
# Consolidate many of these build steps via Makefile.
# Caching of dependencies is a serious problem. Cloud Build does not complete within 20 minutes!

55
cmd/backend/Dockerfile Normal file
View File

@ -0,0 +1,55 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM open-match-base-build as builder
WORKDIR /go/src/open-match.dev/open-match/cmd/backend/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/open-match.dev/open-match/cmd/backend/backend .
ENTRYPOINT ["/backend"]
# Docker Image Arguments
ARG BUILD_DATE
ARG VCS_REF
ARG BUILD_VERSION
ARG IMAGE_TITLE="Open Match Backend API"
# Standardized Docker Image Labels
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
LABEL \
org.opencontainers.image.created="${BUILD_TIME}" \
org.opencontainers.image.authors="Google LLC <open-match-discuss@googlegroups.com>" \
org.opencontainers.image.url="https://open-match.dev/" \
org.opencontainers.image.documentation="https://open-match.dev/site/docs/" \
org.opencontainers.image.source="https://github.com/googleforgames/open-match" \
org.opencontainers.image.version="${BUILD_VERSION}" \
org.opencontainers.image.revision="1" \
org.opencontainers.image.vendor="Google LLC" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.ref.name="" \
org.opencontainers.image.title="${IMAGE_TITLE}" \
org.opencontainers.image.description="Flexible, extensible, and scalable video game matchmaking." \
org.label-schema.schema-version="1.0" \
org.label-schema.build-date=$BUILD_DATE \
org.label-schema.url="http://open-match.dev/" \
org.label-schema.vcs-url="https://github.com/googleforgames/open-match" \
org.label-schema.version=$BUILD_VERSION \
org.label-schema.vcs-ref=$VCS_REF \
org.label-schema.vendor="Google LLC" \
org.label-schema.name="${IMAGE_TITLE}" \
org.label-schema.description="Flexible, extensible, and scalable video game matchmaking." \
org.label-schema.usage="https://open-match.dev/site/docs/"

24
cmd/backend/backend.go Normal file
View File

@ -0,0 +1,24 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package main is the backend service for Open Match.
package main
import (
"open-match.dev/open-match/internal/app/backend"
)
func main() {
backend.RunApplication()
}

View File

@ -1,23 +0,0 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM open-match-base-build as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/cmd/backendapi/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/cmd/backendapi/backendapi .
ENTRYPOINT ["/backendapi"]

View File

@ -1,14 +0,0 @@
/*
BackendAPI contains the unique files required to run the API endpoints for
Open Match's backend. It is assumed you'll either integrate calls to these
endpoints directly into your dedicated game server (simple use case), or call
these endpoints from other, established services in your infrastructure (more
complicated use cases).
Note that the main package for backendapi does very little except read the
config and set up logging and metrics, then start the server. Almost all the
work is being done by backendapi/apisrv, which implements the gRPC server
defined in the backendapi/proto/backend.pb.go file.
*/
package main

View File

@ -1,31 +0,0 @@
/*
This application handles all the startup and connection scaffolding for
running a gRPC server serving the APIService as defined in
${OM_ROOT}/internal/pb/backend.pb.go
All the actual important bits are in the API Server source code: apisrv/apisrv.go
Copyright 2018 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"github.com/GoogleCloudPlatform/open-match/internal/app/backendapi"
)
func main() {
backendapi.RunApplication()
}

55
cmd/evaluator/Dockerfile Normal file
View File

@ -0,0 +1,55 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM open-match-base-build as builder
WORKDIR /go/src/open-match.dev/open-match/cmd/evaluator/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/open-match.dev/open-match/cmd/evaluator/evaluator .
ENTRYPOINT ["/evaluator"]
# Docker Image Arguments
ARG BUILD_DATE
ARG VCS_REF
ARG BUILD_VERSION
ARG IMAGE_TITLE="Open Match Evaluator API"
# Standardized Docker Image Labels
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
LABEL \
org.opencontainers.image.created="${BUILD_TIME}" \
org.opencontainers.image.authors="Google LLC <open-match-discuss@googlegroups.com>" \
org.opencontainers.image.url="https://open-match.dev/" \
org.opencontainers.image.documentation="https://open-match.dev/site/docs/" \
org.opencontainers.image.source="https://github.com/googleforgames/open-match" \
org.opencontainers.image.version="${BUILD_VERSION}" \
org.opencontainers.image.revision="1" \
org.opencontainers.image.vendor="Google LLC" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.ref.name="" \
org.opencontainers.image.title="${IMAGE_TITLE}" \
org.opencontainers.image.description="Flexible, extensible, and scalable video game matchmaking." \
org.label-schema.schema-version="1.0" \
org.label-schema.build-date=$BUILD_DATE \
org.label-schema.url="http://open-match.dev/" \
org.label-schema.vcs-url="https://github.com/googleforgames/open-match" \
org.label-schema.version=$BUILD_VERSION \
org.label-schema.vcs-ref=$VCS_REF \
org.label-schema.vendor="Google LLC" \
org.label-schema.name="${IMAGE_TITLE}" \
org.label-schema.description="Flexible, extensible, and scalable video game matchmaking." \
org.label-schema.usage="https://open-match.dev/site/docs/"

View File

@ -0,0 +1,29 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package main is the evaluator service for Open Match.
package main
import (
"open-match.dev/open-match/internal/app/evaluator"
"open-match.dev/open-match/internal/pb"
)
func main() {
evaluator.RunApplication(&evaluator.FunctionSettings{Func: evaluate})
}
func evaluate(candidates []*pb.Match) []*pb.Match {
return candidates
}

55
cmd/frontend/Dockerfile Normal file
View File

@ -0,0 +1,55 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM open-match-base-build as builder
WORKDIR /go/src/open-match.dev/open-match/cmd/frontend/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/open-match.dev/open-match/cmd/frontend/frontend .
ENTRYPOINT ["/frontend"]
# Docker Image Arguments
ARG BUILD_DATE
ARG VCS_REF
ARG BUILD_VERSION
ARG IMAGE_TITLE="Open Match Frontend API"
# Standardized Docker Image Labels
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
LABEL \
org.opencontainers.image.created="${BUILD_TIME}" \
org.opencontainers.image.authors="Google LLC <open-match-discuss@googlegroups.com>" \
org.opencontainers.image.url="https://open-match.dev/" \
org.opencontainers.image.documentation="https://open-match.dev/site/docs/" \
org.opencontainers.image.source="https://github.com/googleforgames/open-match" \
org.opencontainers.image.version="${BUILD_VERSION}" \
org.opencontainers.image.revision="1" \
org.opencontainers.image.vendor="Google LLC" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.ref.name="" \
org.opencontainers.image.title="${IMAGE_TITLE}" \
org.opencontainers.image.description="Flexible, extensible, and scalable video game matchmaking." \
org.label-schema.schema-version="1.0" \
org.label-schema.build-date=$BUILD_DATE \
org.label-schema.url="http://open-match.dev/" \
org.label-schema.vcs-url="https://github.com/googleforgames/open-match" \
org.label-schema.version=$BUILD_VERSION \
org.label-schema.vcs-ref=$VCS_REF \
org.label-schema.vendor="Google LLC" \
org.label-schema.name="${IMAGE_TITLE}" \
org.label-schema.description="Flexible, extensible, and scalable video game matchmaking." \
org.label-schema.usage="https://open-match.dev/site/docs/"

24
cmd/frontend/frontend.go Normal file
View File

@ -0,0 +1,24 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package main is the frontend service for Open Match.
package main
import (
"open-match.dev/open-match/internal/app/frontend"
)
func main() {
frontend.RunApplication()
}

View File

@ -1,23 +0,0 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM open-match-base-build as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/cmd/frontendapi/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/cmd/frontendapi/frontendapi .
ENTRYPOINT ["/frontendapi"]

View File

@ -1,14 +0,0 @@
/*
FrontendAPI contains the unique files required to run the API endpoints for
Open Match's frontend. It is assumed you'll either integrate calls to these
endpoints directly into your game client (simple use case), or call these
endpoints from other, established platform services in your infrastructure
(more complicated use cases).
Note that the main package for frontendapi does very little except read the
config and set up logging and metrics, then start the server. Almost all the
work is being done by frontendapi/apisrv, which implements the gRPC server
defined in the frontendapi/proto/frontend.pb.go file.
*/
package main

View File

@ -1,31 +0,0 @@
/*
This application handles all the startup and connection scaffolding for
running a gRPC server serving the APIService as defined in
${OM_ROOT}/internal/pb/frontend.pb.go
All the actual important bits are in the API Server source code: apisrv/apisrv.go
Copyright 2018 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"github.com/GoogleCloudPlatform/open-match/internal/app/frontendapi"
)
func main() {
frontendapi.RunApplication()
}

View File

@ -1,10 +1,55 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM open-match-base-build as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/cmd/minimatch/
WORKDIR /go/src/open-match.dev/open-match/cmd/minimatch/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/cmd/minimatch/minimatch .
COPY --from=builder /go/src/open-match.dev/open-match/cmd/minimatch/minimatch .
ENTRYPOINT ["/minimatch"]
# Docker Image Arguments
ARG BUILD_DATE
ARG VCS_REF
ARG BUILD_VERSION
ARG IMAGE_TITLE="Mini Match"
# Standardized Docker Image Labels
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
LABEL \
org.opencontainers.image.created="${BUILD_TIME}" \
org.opencontainers.image.authors="Google LLC <open-match-discuss@googlegroups.com>" \
org.opencontainers.image.url="https://open-match.dev/" \
org.opencontainers.image.documentation="https://open-match.dev/site/docs/" \
org.opencontainers.image.source="https://github.com/googleforgames/open-match" \
org.opencontainers.image.version="${BUILD_VERSION}" \
org.opencontainers.image.revision="1" \
org.opencontainers.image.vendor="Google LLC" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.ref.name="" \
org.opencontainers.image.title="${IMAGE_TITLE}" \
org.opencontainers.image.description="Flexible, extensible, and scalable video game matchmaking." \
org.label-schema.schema-version="1.0" \
org.label-schema.build-date=$BUILD_DATE \
org.label-schema.url="http://open-match.dev/" \
org.label-schema.vcs-url="https://github.com/googleforgames/open-match" \
org.label-schema.version=$BUILD_VERSION \
org.label-schema.vcs-ref=$VCS_REF \
org.label-schema.vendor="Google LLC" \
org.label-schema.name="${IMAGE_TITLE}" \
org.label-schema.description="Flexible, extensible, and scalable video game matchmaking." \
org.label-schema.usage="https://open-match.dev/site/docs/"

View File

@ -1,27 +1,22 @@
/*
This application is a minified version of Open Match.
All the actual important bits are in the API Server source code: apisrv/apisrv.go
Copyright 2019 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package main is the minimatch in-process testing binary for Open Match.
package main
import (
"github.com/GoogleCloudPlatform/open-match/internal/app/minimatch"
"open-match.dev/open-match/internal/app/minimatch"
)
func main() {

55
cmd/mmlogic/Dockerfile Normal file
View File

@ -0,0 +1,55 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM open-match-base-build as builder
WORKDIR /go/src/open-match.dev/open-match/cmd/mmlogic/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/open-match.dev/open-match/cmd/mmlogic/mmlogic .
ENTRYPOINT ["/mmlogic"]
# Docker Image Arguments
ARG BUILD_DATE
ARG VCS_REF
ARG BUILD_VERSION
ARG IMAGE_TITLE="Open Match Data API"
# Standardized Docker Image Labels
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
LABEL \
org.opencontainers.image.created="${BUILD_TIME}" \
org.opencontainers.image.authors="Google LLC <open-match-discuss@googlegroups.com>" \
org.opencontainers.image.url="https://open-match.dev/" \
org.opencontainers.image.documentation="https://open-match.dev/site/docs/" \
org.opencontainers.image.source="https://github.com/googleforgames/open-match" \
org.opencontainers.image.version="${BUILD_VERSION}" \
org.opencontainers.image.revision="1" \
org.opencontainers.image.vendor="Google LLC" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.ref.name="" \
org.opencontainers.image.title="${IMAGE_TITLE}" \
org.opencontainers.image.description="Flexible, extensible, and scalable video game matchmaking." \
org.label-schema.schema-version="1.0" \
org.label-schema.build-date=$BUILD_DATE \
org.label-schema.url="http://open-match.dev/" \
org.label-schema.vcs-url="https://github.com/googleforgames/open-match" \
org.label-schema.version=$BUILD_VERSION \
org.label-schema.vcs-ref=$VCS_REF \
org.label-schema.vendor="Google LLC" \
org.label-schema.name="${IMAGE_TITLE}" \
org.label-schema.description="Flexible, extensible, and scalable video game matchmaking." \
org.label-schema.usage="https://open-match.dev/site/docs/"

24
cmd/mmlogic/mmlogic.go Normal file
View File

@ -0,0 +1,24 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package main is the mmlogic service for Open Match.
package main
import (
"open-match.dev/open-match/internal/app/mmlogic"
)
func main() {
mmlogic.RunApplication()
}

View File

@ -1,23 +0,0 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM open-match-base-build as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/cmd/mmlogicapi/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/cmd/mmlogicapi/mmlogicapi .
ENTRYPOINT ["/mmlogicapi"]

View File

@ -1,30 +0,0 @@
/*
This application handles all the startup and connection scaffolding for
running a gRPC server serving the APIService as defined in
${OM_ROOT}/internal/pb/mmlogic.pb.go
All the actual important bits are in the API Server source code: apisrv/apisrv.go
Copyright 2018 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"github.com/GoogleCloudPlatform/open-match/internal/app/mmlogicapi"
)
func main() {
mmlogicapi.RunApplication()
}

View File

@ -1,33 +0,0 @@
/*
Package config contains convenience functions for reading and managing configuration.
Copyright 2019 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
import (
"testing"
)
func TestReadConfig(t *testing.T) {
cfg, err := Read()
if err != nil {
t.Fatalf("cannot load config, %s", err)
}
if cfg.GetString("metrics.endpoint") != "/metrics" {
t.Errorf("av.GetString('metrics.endpoint') = %s, expected '/metrics'", cfg.GetString("metrics.endpoint"))
}
}

View File

@ -1,90 +0,0 @@
# kubectl create configmap om-configmap --from-file=config/matchmaker_config.yaml
debug: true
logging:
level: debug
format: text
source: false
api:
backend:
hostname: om-backendapi
port: 50505
backoff: "[2 32] *2 ~0.33 <30"
proxyport: 51505
frontend:
hostname: om-frontendapi
port: 50504
backoff: "[2 32] *2 ~0.33 <300"
proxyport: 51504
mmlogic:
hostname: om-mmlogicapi
port: 50503
proxyport: 51503
functions:
port: 50502
proxyport: 51502
evaluator:
# Evaluator intervals are in milliseconds
pollIntervalMs: 1000
maxWaitMs: 10000
metrics:
port: 9555
endpoint: /metrics
reportingPeriod: 5
queues:
proposals:
name: proposalq
ignoreLists:
proposed:
name: proposed
offset: 0
duration: 800
deindexed:
name: deindexed
offset: 0
duration: 800
expired:
name: OM_METADATA.accessed
offset: 800
duration: 0
redis:
pool:
maxIdle: 3
maxActive: 0
idleTimeout: 60
queryArgs:
count: 10000
results:
pageSize: 10000
expirations:
player: 43200
matchobject: 43200
jsonkeys:
rosters: properties.rosters
pools: properties.pools
playerIndices:
- char.cleric
- char.knight
- char.paladin
- map.aleroth
- map.oasis
- mmr.rating
- mode.battleroyale
- mode.ctf
- mode.demo
- region.europe-east1
- region.europe-west1
- region.europe-west2
- region.europe-west3
- region.europe-west4
- role.dps
- role.support
- role.tank

View File

@ -1,46 +0,0 @@
/*
Package config contains convenience functions for reading and managing configuration.
Copyright 2019 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
import (
"time"
"github.com/spf13/viper"
)
// View is a read-only view of the Open Match configuration.
// New accessors from Viper should be added here.
type View interface {
IsSet(string) bool
GetString(string) string
GetInt(string) int
GetInt64(string) int64
GetStringSlice(string) []string
GetBool(string) bool
GetDuration(string) time.Duration
GetStringMap(string) map[string]interface{}
}
// Sub returns a subset of configuration filtered by the key.
func Sub(v View, key string) View {
vcfg, ok := v.(*viper.Viper)
if ok {
return vcfg.Sub(key)
}
return nil
}

30
doc.go
View File

@ -1,18 +1,16 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package openmatch provides flexible, extensible, and scalable video game matchmaking.
package openmatch
package openmatch // import "open-match.dev/open-match"

View File

@ -1,84 +0,0 @@
## Building
Documentation and usage guides on how to set up and customize Open Match.
### Precompiled container images
Once we reach a 1.0 release, we plan to produce publicly available (Linux) Docker container images of major releases in a public image registry. Until then, refer to the 'Compiling from source' section below.
### Compiling from source
The easiest way to build Open Match is to use the Makefile. Before you can use the Makefile make sure you have the following dependencies:
```bash
# Install Open Match Toolchain Dependencies (Debian other OSes including Mac OS X have similar dependencies)
sudo apt-get update; sudo apt-get install -y -q python3 python3-virtualenv virtualenv make google-cloud-sdk git unzip tar
# Setup your repository like Go workspace, https://golang.org/doc/code.html#Workspaces
# This requirement will go away soon.
mkdir -p workspace/src/github.com/GoogleCloudPlatform/
cd workspace/src/github.com/GoogleCloudPlatform/
export GOPATH=$HOME/workspace
export GO111MODULE=on
git clone https://github.com/GoogleCloudPlatform/open-match.git
cd open-match
```
[Docker](https://docs.docker.com/install/) and [Go 1.11+](https://golang.org/dl/) is also required. If your distro is new enough you can probably run `sudo apt-get install -y golang` or download the newest version from https://golang.org/.
To build all the artifacts of Open Match you can simply run the following commands.
```bash
# Downloads all the tools needed to build Open Match
make install-toolchain
# Generates protocol buffer code files
make all-protos
# Builds all the binaries
make all
# Builds all the images.
make build-images
```
Once build you can use a command like `docker images` to see all the images that were build.
Before creating a pull request you can run `make local-cloud-build` to simulate a Cloud Build run to check for regressions.
The directory structure is a typical Go structure so if you do the following you should be able to work on this project within your IDE.
```bash
cd $GOPATH
mkdir -p src/github.com/GoogleCloudPlatform/
cd src/github.com/GoogleCloudPlatform/
# If you're going to contribute you'll want to fork open-match, see CONTRIBUTING.md for details.
git clone https://github.com/GoogleCloudPlatform/open-match.git
cd open-match
# Open IDE in this directory.
```
Lastly, this project uses go modules so you'll want to set `export GO111MODULE=on` before building.
## Zero to Open Match
To deploy Open Match quickly to a Kubernetes cluster run these commands.
```bash
# Downloads all the tools.
make install-toolchain
# Create a GKE Cluster
make create-gke-cluster
# OR Create a Minikube Cluster
make create-mini-cluster
# Install Helm
make push-helm
# Build and push images
make push-images -j4
# Deploy Open Match with example functions
make install-chart install-example-chart
```
## Docker Image Builds
All the core components for Open Match are written in Golang and use the [Dockerfile multistage builder pattern](https://docs.docker.com/develop/develop-images/multistage-build/). This pattern uses intermediate Docker containers as a Golang build environment while producing lightweight, minimized container images as final build artifacts. When the project is ready for production, we will modify the `Dockerfile`s to uncomment the last build stage. Although this pattern is great for production container images, it removes most of the utilities required to troubleshoot issues during development.
## Configuration
Currently, each component reads a local config file `matchmaker_config.json`, and all components assume they have the same configuration. To this end, there is a single centralized config file located in the `<REPO_ROOT>/config/` which is symlinked to each component's subdirectory for convenience when building locally. When `docker build`ing the component container images, the Dockerfile copies the centralized config file into the component directory.
We plan to replace this with a Kubernetes-managed config with dynamic reloading, please join the discussion in [Issue #42](issues/42).

View File

@ -1,4 +1,3 @@
# Core Concepts
[Watch the introduction of Open Match at Unite Berlin 2018 on YouTube](https://youtu.be/qasAmy_ko2o)
@ -8,19 +7,23 @@ Open Match is designed to support massively concurrent matchmaking, and to be sc
## Glossary
### General
* **DGS** &mdash; Dedicated game server
* **Client** &mdash; The game client program the player uses when playing the game
* **Session** &mdash; In Open Match, players are matched together, then assigned to a server which hosts the game _session_. Depending on context, this may be referred to as a _match_, _map_, or just _game_ elsewhere in the industry.
* **Session** &mdash; In Open Match, players are matched together, then assigned to a server which hosts the game _session_. Depending on context, this may be referred to as a _match_, _map_, or just _game_ elsewhere in the industry.
### Open Match
* **Component** &mdash; One of the discrete processes in an Open Match deployment. Open Match is composed of multiple scalable microservices called _components_.
* **State Storage** &mdash; The storage software used by Open Match to hold all the matchmaking state. Open Match ships with [Redis](https://redis.io/) as the default state storage.
* **MMFOrc** &mdash; Matchmaker function orchestrator. This Open Match core component is in charge of kicking off custom matchmaking functions (MMFs) and evaluator processes.
* **MMF** &mdash; Matchmaking function. This is the customizable matchmaking logic.
* **MMLogic API** &mdash; An API that provides MMF SDK functionality. It is optional - you can also do all the state storage read and write operations yourself if you have a good reason to do so.
* **Function Harness** &mdash; A GRPC serving harness that triggers the Match function.
* **Evaluator** &mdash; Customizable evaluation logic that analyzes match proposals and approves / rejects matches.
* **MMLogic API** &mdash; An API that provides MMF SDK functionality.
* **Director** &mdash; The software you (as a developer) write against the Open Match Backend API. The _Director_ decides which MMFs to run, and is responsible for sending MMF results to a DGS to host the session.
### Data Model
### Data Model
* **Player** &mdash; An ID and list of attributes with values for a player who wants to participate in matchmaking.
* **Roster** &mdash; A list of player objects. Used to hold all the players on a single team.
* **Filter** &mdash; A _filter_ is used to narrow down the players to only those who have an attribute value within a certain integer range. All attributes are integer values in Open Match because [that is how indices are implemented](internal/statestorage/redis/playerindices/playerindices.go). A _filter_ is defined in a _player pool_.
@ -31,6 +34,7 @@ Open Match is designed to support massively concurrent matchmaking, and to be sc
* **Ignore List** &mdash; Removing players from matchmaking consideration is accomplished using _ignore lists_. They contain lists of player IDs that your MMF should not include when making matches.
## Requirements
* [Kubernetes](https://kubernetes.io/) cluster &mdash; tested with version 1.11.7.
* [Redis 4+](https://redis.io/) &mdash; tested with 4.0.11.
* Open Match is compiled against the latest release of [Golang](https://golang.org/) &mdash; tested with 1.11.5.
@ -39,17 +43,14 @@ Open Match is designed to support massively concurrent matchmaking, and to be sc
Open Match is a set of processes designed to run on Kubernetes. It contains these **core** components:
1. Frontend API
1. Backend API
1. Matchmaker Function Orchestrator (MMFOrc) (may be deprecated in future versions)
* Frontend API
* Backend API
* Matchmaking Logic (MMLogic) API
It includes these **optional** (but recommended) components:
1. Matchmaking Logic (MMLogic) API
It also depends on these two **customizable** components.
It also explicitly depends on these two **customizable** components.
1. Matchmaking "Function" (MMF)
1. Evaluator (may be optional in future versions)
* Match Function (MMF)
* Evaluator
While **core** components are fully open source and _can_ be modified, they are designed to support the majority of matchmaking scenarios *without need to change the source code*. The Open Match repository ships with simple **customizable** MMF and Evaluator examples, but it is expected that most users will want full control over the logic in these, so they have been designed to be as easy to modify or replace as possible.
@ -71,16 +72,9 @@ The Backend API is a server application that implements the [gRPC](https://grpc.
* A **unique ID** for a matchmaking profile.
* A **json blob** containing all the matching-related data and filters you want to use in your matchmaking function.
* An optional list of **roster**s to hold the resulting teams chosen by your matchmaking function.
* An optional set of **filters** that define player pools your matchmaking function will choose players from.
* An optional set of **filters** that define player pools your matchmaking function will choose players from.
Your game backend is expected to maintain a connection, waiting for 'filled' match objects containing a roster of players. The Backend API also provides a return path for your game backend to return dedicated game server connection details (an 'assignment') to the game client, and to delete these 'assignments'.
### Matchmaking Function Orchestrator (MMFOrc)
The MMFOrc kicks off your custom matchmaking function (MMF) for every unique profile submitted to the Backend API in a match object. It also runs the Evaluator to resolve conflicts in case more than one of your profiles matched the same players.
The MMFOrc exists to orchestrate/schedule your **custom components**, running them as often as required to meet the demands of your game. MMFOrc runs in an endless loop, submitting MMFs and Evaluator jobs to Kubernetes.
Your game backend is expected to maintain a connection, waiting for 'filled' match objects containing a roster of players. The Backend API also provides a return path for your game backend to return dedicated game server connection details (an 'assignment') to the game client, and to delete these 'assignments'.
### Matchmaking Logic (MMLogic) API
@ -98,39 +92,23 @@ More details about the available gRPC calls can be found in the [API Specificati
### Evaluator
The Evaluator resolves conflicts when multiple MMFs select the same player(s).
The Evaluator resolves conflicts when multiple MMFs select the same player(s). Evaluator is provided by the developer (sample included in Open Match).
The Evaluator is a component run by the Matchmaker Function Orchestrator (MMFOrc) after the matchmaker functions have been run, and some proposed results are available. The Evaluator looks at all the proposals, and if multiple proposals contain the same player(s), it breaks the tie. In many simple matchmaking setups with only a few game modes and well-tuned matchmaking functions, the Evaluator may functionally be a no-op or first-in-first-out algorithm. In complex matchmaking setups where, for example, a player can queue for multiple types of matches, the Evaluator provides the critical customizability to evaluate all available proposals and approve those that will passed to your game servers.
The Evaluator runs forever, looping over a configured interval, checking if MMFs have completed execution or if certain time interval has passed. Upon reaching those conditions, the Evaluator calls the Evaluation Function (to be modified by the user) with the proposals to choose from. The sample Evaluation function looks at all the proposals, and if multiple proposals contain the same player(s), it breaks the tie. In many simple matchmaking setups with only a few game modes and well-tuned matchmaking functions, the Evaluator may functionally be a no-op or first-in-first-out algorithm. In complex matchmaking setups where, for example, a player can queue for multiple types of matches, the Evaluator provides the critical customizability to evaluate all available proposals and approve those that will passed to your game servers.
Large-scale concurrent matchmaking functions is a complex topic, and users who wish to do this are encouraged to engage with the [Open Match community](https://github.com/GoogleCloudPlatform/open-match#get-involved) about patterns and best practices.
Large-scale concurrent matchmaking functions is a complex topic, and users who wish to do this are encouraged to engage with the [Open Match community](https://github.com/googleforgames/open-match#get-involved) about patterns and best practices.
### Matchmaking Functions (MMFs)
Matchmaking Functions (MMFs) are run by the Matchmaker Function Orchestrator (MMFOrc) &mdash; once per profile it sees in state storage. The MMF is run as a Job in Kubernetes, and has full access to read and write from state storage. At a high level, the encouraged pattern is to write a MMF in whatever language you are comfortable in that can do the following things:
Matchmaking Functions (MMFs) are implemented by the developer and are hosted as a gRPC service. Open Match provides a harness (currently for golang) that handles the broiler-plate Open Match communitation, gRPC server setup etc., so that the user only has to write a function that accepts a set of player pools and a match profile and returns a proposal based on some core match making logic. An MMF is called each time a request to generate a match is received. At a high level, an MMF needs to generate a proposal using the given players, match profile and its custom match making logic and return the proposal to the calling harness.
**Note**: Currently Open Match only has a golang harness. To add an MMF in any other language, a harness needs to be implemented in that language.
- [x] Be packaged in a (Linux) Docker container.
- [x] Read/write from the Open Match state storage &mdash; Open Match ships with Redis as the default state storage.
- [x] Read a profile you wrote to state storage using the Backend API.
- [x] Select from the player data you wrote to state storage using the Frontend API. It must respect all the ignore lists defined in the matchmaker config.
- [ ] Run your custom logic to try to find a match.
- [x] Write the match object it creates to state storage at a specified key.
- [x] Remove the players it selected from consideration by other MMFs by adding them to the appropriate ignore list.
- [x] Notify the MMFOrc of completion.
- [x] (Optional, but recommended) Export stats for metrics collection.
## Example Tooling
**Open Match offers [matchmaking logic API](#matchmaking-logic-mmlogic-api) calls for handling the checked items, as long as you are able to format your input and output in the data schema Open Match expects (defined in the [protobuf messages](api/protobuf-spec/messages.proto)).** You can to do this work yourself if you don't want to or can't use the data schema Open Match is looking for. However, the data formats expected by Open Match are pretty generalized and will work with most common matchmaking scenarios and game types. If you have questions about how to fit your data into the formats specified, feel free to ask us in the [Slack or mailing group](#get-involved).
To see Open Match, in action, here are some basic tools that are provided as samples:
Example MMFs are provided in these languages:
- [C#](examples/functions/csharp/simple) (doesn't use the MMLogic API)
- [Python3](examples/functions/python3/mmlogic-simple) (MMLogic API enabled)
- [PHP](examples/functions/php/mmlogic-simple) (MMLogic API enabled)
- [golang](examples/functions/golang/manual-simple) (doesn't use the MMLogic API)
* `test/cmd/clientloadgen/` is a (VERY) basic client load simulation tool. It endlessly writes players into state storage so you can test your backend integration, and run your custom MMFs and Evaluators (which are only triggered when there are players in the pool).
## Additional examples
* `examples/backendclient` is a fake client for the Backend API. It pretends to be a dedicated game server backend connecting to Open Match and sending in a match profile to fill and receives completed matches. It can call Create / List matches.
**Note:** These examples will be expanded on in future releases.
The following examples of how to call the APIs are provided in the repository. Both have a `Dockerfile` and `cloudbuild.yaml` files in their respective directories:
* `test/cmd/frontendclient/main.go` acts as a client to the the Frontend API, putting a player into the queue with simulated latencies from major metropolitan cities and a couple of other matchmaking attributes. It then waits for you to manually put a value in Redis to simulate a server connection string being written using the backend API 'CreateAssignments' call, and displays that value on stdout for you to verify.
* `examples/backendclient/main.go` calls the Backend API and passes in the profile found in `backendstub/profiles/testprofile.json` to the `ListMatches` API endpoint, then continually prints the results until you exit, or there are insufficient players to make a match based on the profile..
* `test/cmd/frontendclient/` is a fake client for the Frontend API. It pretends to be group of real game clients connecting to Open Match. It requests a game, then dumps out the results each player receives to the screen.

View File

@ -1,188 +1,184 @@
# Development Guide
This doc explains how to setup a development environment so you can get started contributing to Open Match. If you instead want to write a matchmaker that _uses_ Open Match, you probably want to read the [User Guide](user_guide.md).
Open Match is a collection of [Go](https://golang.org/) applications that run
within [Kubernetes](https://kubernetes.io).
# Compiling from source
If you're not familiar with either that's ok, you can checkout the
[reference material](references.md). Knowledge of these tools is not
required to setup your environment.
All components of Open Match produce (Linux) Docker container images as artifacts, and there are included `Dockerfile`s for each. [Google Cloud Platform Cloud Build](https://cloud.google.com/cloud-build/docs/) users will also find `cloudbuild.yaml` files for each component in their respective directories. Note that most of them build from an 'base' image called `openmatch-devbase`. You can find a `Dockerfile` and `cloudbuild_base.yaml` file for this in the repository root. Build it first!
## Install Prerequisites
Note: Although Google Cloud Platform includes some free usage, you may incur charges following this guide if you use GCP products.
To build Open Match you'll need the following applications installed.
## Security Disclaimer
**This project has not completed a first-line security audit, and there are definitely going to be some service accounts that are too permissive. This should be fine for testing/development in a local environment, but absolutely should not be used as-is in a production environment without your team/organization evaluating it's permissions.**
* [Git](https://git-scm.com/downloads)
* [Go](https://golang.org/doc/install)
* [Python3 with virtualenv](https://wiki.python.org/moin/BeginnersGuide/Download)
* Make (Mac: install [XCode](https://itunes.apple.com/us/app/xcode/id497799835))
* [Docker](https://docs.docker.com/install/) including the
[post-install steps](https://docs.docker.com/install/linux/linux-postinstall/).
## Before getting started
**NOTE**: Before starting with this guide, you'll need to update all the URIs from the tutorial's gcr.io container image registry with the URI for your own image registry. If you are using the gcr.io registry on GCP, the default URI is `gcr.io/<PROJECT_NAME>`. Here's an example command in Linux to do the replacement for you this (replace YOUR_REGISTRY_URI with your URI, this should be run from the repository root directory):
```
# Linux
egrep -lR 'matchmaker-dev-201405' . | xargs sed -i -e 's|matchmaker-dev-201405|<PROJECT_NAME>|g'
```
```
# Mac OS, you can delete the .backup files after if all looks good
egrep -lR 'matchmaker-dev-201405' . | xargs sed -i'.backup' -e 's|matchmaker-dev-201405|<PROJECT_NAME>|g'
Optional Software
* [Google Cloud Platform](gcloud.md)
* [Visual Studio Code](https://code.visualstudio.com/Download) for IDE.
Vim and Emacs work to.
* [VirtualBox](https://www.virtualbox.org/wiki/Downloads) recommended for
[Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/).
On Debian-based Linux you can install all the required packages (except Go) by
running:
```bash
sudo apt-get update
sudo apt-get install -y -q python3 python3-virtualenv virtualenv make \
google-cloud-sdk git unzip tar
```
## Example of building using Google Cloud Builder
*It's recommended that you install Go using their instructions because package
managers tend to lag behind the latest Go releases.*
The [Quickstart for Docker](https://cloud.google.com/cloud-build/docs/quickstart-docker) guide explains how to set up a project, enable billing, enable Cloud Build, and install the Cloud SDK if you haven't do these things before. Once you get to 'Preparing source files' you are ready to continue with the steps below.
## Get the Code
* Clone this repo to a local machine or Google Cloud Shell session, and cd into it.
* In Linux, you can run the following one-line bash script to compile all the images for the first time, and push them to your gcr.io registry. You must enable the [Container Registry API](https://console.cloud.google.com/flows/enableapi?apiid=containerregistry.googleapis.com) first.
```
# First, build the 'base' image. Some other images depend on this so it must complete first.
gcloud builds submit --config cloudbuild_base.yaml
# Build all other images.
for dfile in $(find . -name "Dockerfile" -iregex "./\(cmd\|test\|examples\)/.*"); do cd $(dirname ${dfile}); gcloud builds submit --config cloudbuild.yaml & cd -; done
```
Note: as of v0.3.0 alpha, the Python and PHP MMF examples still depend on the previous way of building until [issue #42, introducing new config management](https://github.com/GoogleCloudPlatform/open-match/issues/42) is resolved (apologies for the inconvenience):
```
gcloud builds submit --config cloudbuild_mmf_py3.yaml
gcloud builds submit --config cloudbuild_mmf_php.yaml
```
* Once the cloud builds have completed, you can verify that all the builds succeeded in the cloud console or by by checking the list of images in your **gcr.io** registry:
```
gcloud container images list
```
(your registry name will be different)
```
NAME
gcr.io/matchmaker-dev-201405/openmatch-backendapi
gcr.io/matchmaker-dev-201405/openmatch-devbase
gcr.io/matchmaker-dev-201405/openmatch-evaluator
gcr.io/matchmaker-dev-201405/openmatch-frontendapi
gcr.io/matchmaker-dev-201405/openmatch-mmf-golang-manual-simple
gcr.io/matchmaker-dev-201405/openmatch-mmf-php-mmlogic-simple
gcr.io/matchmaker-dev-201405/openmatch-mmf-py3-mmlogic-simple
gcr.io/matchmaker-dev-201405/openmatch-mmforc
gcr.io/matchmaker-dev-201405/openmatch-mmlogicapi
```
## Example of starting a GKE cluster
A cluster with mostly default settings will work for this development guide. In the Cloud SDK command below we start it with machines that have 4 vCPUs. Alternatively, you can use the 'Create Cluster' button in [Google Cloud Console]("https://console.cloud.google.com/kubernetes").
```
gcloud container clusters create --machine-type n1-standard-4 open-match-dev-cluster --zone <ZONE>
```bash
# Create a directory for the project.
mkdir -p $HOME/workspace
cd $HOME/workspace
# Download the source code.
git clone https://github.com/googleforgames/open-match.git
cd open-match
# Print the help for the Makefile commands.
make
```
If you don't know which zone to launch the cluster in (`<ZONE>`), you can list all available zones by running the following command.
*Typically for contributing you'll want to
[create a fork](https://help.github.com/en/articles/fork-a-repo) and use that
but for purpose of this guide we'll be using the upstream/master.*
```
gcloud compute zones list
## Building
```bash
# Reset workspace
make clean
# Compile all the binaries
make all -j$(nproc)
# Run tests
make test
# Build all the images.
make build-images -j$(nproc)
# Push images to gcr.io (requires Google Cloud SDK installed)
make push-images -j$(nproc)
# Push images to Docker Hub
make REGISTRY=mydockerusername push-images -j$(nproc)
```
## Configuration
_**-j$(nproc)** is a flag to tell make to parallelize the commands based on
the number of CPUs on your machine._
Currently, each component reads a local config file `matchmaker_config.json`, and all components assume they have the same configuration (if you would like to help us design the replacement config solution, please join the [discussion](https://github.com/GoogleCloudPlatform/open-match/issues/42). To this end, there is a single centralized config file located in the `<REPO_ROOT>/config/` which is symlinked to each component's subdirectory for convenience when building locally. Note: [there is an issue with symlinks on Windows](../issues/57).
## Deploying to Kubernetes
## Running Open Match in a development environment
Kubernetes comes in many flavors and Open Match can be used in any of them.
The rest of this guide assumes you have a cluster (example is using GKE, but works on any cluster with a little tweaking), and kubectl configured to administer that cluster, and you've built all the Docker container images described by `Dockerfiles` in the repository root directory and given them the docker tag 'dev'. It assumes you are in the `<REPO_ROOT>/deployments/k8s/` directory.
_We support GKE ([setup guide](gcloud.md)), Minikube, and Kubernetes in Docker (KinD) in the Makefile.
As long as kubectl is configured to talk to your Kubernetes cluster as the
default context the Makefile will honor that._
* Start a copy of redis and a service in front of it:
```
kubectl apply -f redis_deployment.yaml
kubectl apply -f redis_service.yaml
```
* Run the **core components**: the frontend API, the backend API, the matchmaker function orchestrator (MMFOrc), and the matchmaking logic API.
**NOTE** In order to kick off jobs, the matchmaker function orchestrator needs a service account with permission to administer the cluster. This should be updated to have min required perms before launch, this is pretty permissive but acceptable for closed testing:
```
kubectl apply -f backendapi_deployment.yaml
kubectl apply -f backendapi_service.yaml
kubectl apply -f frontendapi_deployment.yaml
kubectl apply -f frontendapi_service.yaml
kubectl apply -f mmforc_deployment.yaml
kubectl apply -f mmforc_serviceaccount.yaml
kubectl apply -f mmlogicapi_deployment.yaml
kubectl apply -f mmlogicapi_service.yaml
```
* [optional, but recommended] Configure the OpenCensus metrics services:
```
kubectl apply -f metrics_services.yaml
```
* [optional] Trying to apply the Kubernetes Prometheus Operator resource definition files without a cluster-admin rolebinding on GKE doesn't work without running the following command first. See https://github.com/coreos/prometheus-operator/issues/357
```
kubectl create clusterrolebinding projectowner-cluster-admin-binding --clusterrole=cluster-admin --user=<GCP_ACCOUNT>
```
* [optional, uses beta software] If using Prometheus as your metrics gathering backend, configure the [Prometheus Kubernetes Operator](https://github.com/coreos/prometheus-operator):
```
kubectl apply -f prometheus_operator.yaml
kubectl apply -f prometheus.yaml
kubectl apply -f prometheus_service.yaml
kubectl apply -f metrics_servicemonitor.yaml
```
You should now be able to see the core component pods running using a `kubectl get pods`, and the core component metrics in the Prometheus Web UI by running `kubectl proxy <PROMETHEUS_POD_NAME> 9090:9090` in your local shell, then opening http://localhost:9090/targets in your browser to see which services Prometheus is collecting from.
```bash
# Step 1: Create a Kubernetes (k8s) cluster
# KinD cluster: make create-kind-cluster/delete-kind-cluster
# GKE cluster: make create-gke-cluster/delete-gke-cluster
# or create a local Minikube cluster
make create-gke-cluster
# Step 2: Download helm and install Tiller in the cluster
make push-helm
# Step 3: Build and Push Open Match Images to gcr.io
make push-images -j$(nproc)
# Install Open Match in the cluster.
make install-chart
Here's an example output from `kubectl get all` if everything started correctly, and you included all the optional components (note: this could become out-of-date with upcoming versions; apologies if that happens):
```
NAME READY STATUS RESTARTS AGE
pod/om-backendapi-84bc9d8fff-q89kr 1/1 Running 0 9m
pod/om-frontendapi-55d5bb7946-c5ccb 1/1 Running 0 9m
pod/om-mmforc-85bfd7f4f6-wmwhc 1/1 Running 0 9m
pod/om-mmlogicapi-6488bc7fc6-g74dm 1/1 Running 0 9m
pod/prometheus-operator-5c8774cdd8-7c5qm 1/1 Running 0 9m
pod/prometheus-prometheus-0 2/2 Running 0 9m
pod/redis-master-9b6b86c46-b7ggn 1/1 Running 0 9m
# Create a proxy to Open Match pods so that you can access them locally.
# This command consumes a terminal window that you can kill via Ctrl+C.
# You can run `curl -X POST http://localhost:51504/v1/frontend/tickets` to send
# a DeleteTicket request to the frontend service in the cluster.
# Then try visiting http://localhost:3000/ and view the graphs.
make proxy
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.59.240.1 <none> 443/TCP 19m
service/om-backend-metrics ClusterIP 10.59.254.43 <none> 29555/TCP 9m
service/om-backendapi ClusterIP 10.59.240.211 <none> 50505/TCP 9m
service/om-frontend-metrics ClusterIP 10.59.246.228 <none> 19555/TCP 9m
service/om-frontendapi ClusterIP 10.59.250.59 <none> 50504/TCP 9m
service/om-mmforc-metrics ClusterIP 10.59.240.59 <none> 39555/TCP 9m
service/om-mmlogicapi ClusterIP 10.59.248.3 <none> 50503/TCP 9m
service/prometheus NodePort 10.59.252.212 <none> 9090:30900/TCP 9m
service/prometheus-operated ClusterIP None <none> 9090/TCP 9m
service/redis ClusterIP 10.59.249.197 <none> 6379/TCP 9m
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
deployment.extensions/om-backendapi 1 1 1 1 9m
deployment.extensions/om-frontendapi 1 1 1 1 9m
deployment.extensions/om-mmforc 1 1 1 1 9m
deployment.extensions/om-mmlogicapi 1 1 1 1 9m
deployment.extensions/prometheus-operator 1 1 1 1 9m
deployment.extensions/redis-master 1 1 1 1 9m
NAME DESIRED CURRENT READY AGE
replicaset.extensions/om-backendapi-84bc9d8fff 1 1 1 9m
replicaset.extensions/om-frontendapi-55d5bb7946 1 1 1 9m
replicaset.extensions/om-mmforc-85bfd7f4f6 1 1 1 9m
replicaset.extensions/om-mmlogicapi-6488bc7fc6 1 1 1 9m
replicaset.extensions/prometheus-operator-5c8774cdd8 1 1 1 9m
replicaset.extensions/redis-master-9b6b86c46 1 1 1 9m
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
deployment.apps/om-backendapi 1 1 1 1 9m
deployment.apps/om-frontendapi 1 1 1 1 9m
deployment.apps/om-mmforc 1 1 1 1 9m
deployment.apps/om-mmlogicapi 1 1 1 1 9m
deployment.apps/prometheus-operator 1 1 1 1 9m
deployment.apps/redis-master 1 1 1 1 9m
NAME DESIRED CURRENT READY AGE
replicaset.apps/om-backendapi-84bc9d8fff 1 1 1 9m
replicaset.apps/om-frontendapi-55d5bb7946 1 1 1 9m
replicaset.apps/om-mmforc-85bfd7f4f6 1 1 1 9m
replicaset.apps/om-mmlogicapi-6488bc7fc6 1 1 1 9m
replicaset.apps/prometheus-operator-5c8774cdd8 1 1 1 9m
replicaset.apps/redis-master-9b6b86c46 1 1 1 9m
NAME DESIRED CURRENT AGE
statefulset.apps/prometheus-prometheus 1 1 9m
# Teardown the install
make delete-chart
```
### End-to-End testing
## IDE Support
**Note**: The programs provided below are just bare-bones manual testing programs with no automation and no claim of code coverage. This sparseness of this part of the documentation is because we expect to discard all of these tools and write a fully automated end-to-end test suite and a collection of load testing tools, with extensive stats output and tracing capabilities before 1.0 release. Tracing has to be integrated first, which will be in an upcoming release.
Open Match is a standard Go project so any IDE that understands that should
work. We use [Go Modules](https://github.com/golang/go/wiki/Modules) which is a
relatively new feature in Go so make sure the IDE you are using was built around
Summer 2019. The latest version of
[Visual Studio Code](https://code.visualstudio.com/download) supports it.
In the end: *caveat emptor*. These tools all work and are quite small, and as such are fairly easy for developers to understand by looking at the code and logging output. They are provided as-is just as a reference point of how to begin experimenting with Open Match integrations.
If your IDE is too old you can create a
[Go workspace](https://golang.org/doc/code.html#Workspaces).
* `test/cmd/frontendclient/` is a fake client for the Frontend API. It pretends to be group of real game clients connecting to Open Match. It requests a game, then dumps out the results each player receives to the screen until you press the enter key. **Note**: If you're using the rest of these test programs, you're probably using the Backend Client below. The default profiles that command sends to the backend look for many more than one player, so if you want to see meaningful results from running this Frontend Client, you're going to need to generate a bunch of fake players using the client load simulation tool at the same time. Otherwise, expect to wait until it times out as your matchmaker never has enough players to make a successful match.
* `examples/backendclient` is a fake client for the Backend API. It pretends to be a dedicated game server backend connecting to openmatch and sending in a match profile to fill. Once it receives a match object with a roster, it will also issue a call to assign the player IDs, and gives an example connection string. If it never seems to get a match, make sure you're adding players to the pool using the other two tools. Note: building this image requires that you first build the 'base' dev image (look for `cloudbuild_base.yaml` and `Dockerfile.base` in the root directory) and then update the first step to point to that image in your registry. This will be simplified in a future release. **Note**: If you run this by itself, expect it to wait about 30 seconds, then return a result of 'insufficient players' and exit - this is working as intended. Use the client load simulation tool below to add players to the pool or you'll never be able to make a successful match.
* `test/cmd/clientloadgen/` is a (VERY) basic client load simulation tool. It does **not** test the Frontend API - in fact, it ignores it and writes players directly to state storage on its own. It doesn't do anything but loop endlessly, writing players into state storage so you can test your backend integration, and run your custom MMFs and Evaluators (which are only triggered when there are players in the pool).
```bash
# Create the Go workspace in $HOME/workspace/ directory.
mkdir -p $HOME/workspace/src/open-match.dev/
cd $HOME/workspace/src/open-match.dev/
# Download the source code.
git clone https://github.com/googleforgames/open-match.git
cd open-match
export GOPATH=$HOME/workspace/
```
### Resources
## Pull Requests
* [Prometheus Operator spec](https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md)
If you want to submit a Pull Request there's some tools to help prepare your
change.
```bash
# Runs code generators, tests, and linters.
make presubmit
```
`make presubmit` catches most of the issues your change can run into. If the
submit checks fail you can run it locally via,
```bash
make local-cloud-build
```
Our [continuous integration](https://console.cloud.google.com/cloud-build/builds?project=open-match-build)
runs against all PRs. In order to see your build results you'll need to
become a member of
[open-match-discuss@googlegroups.com](https://groups.google.com/forum/#!forum/open-match-discuss).
## Makefile
The Makefile is the core of Open Match's build process. There's a lot of
commands but here's a list of the important ones and patterns to remember them.
```bash
# Help
make
# Reset workspace (delete all build artifacts)
make clean
# Delete auto-generated protobuf code and swagger API docs.
make clean-protos clean-swagger-docs
# make clean-* deletes some part of the build outputs.
# Build all Docker images
make build-images
# Build frontend docker image.
make build-frontend-image
# Formats, Vets, and tests the codebase.
make fmt vet test
# Same as above also regenerates autogen files.
make presubmit
# Run website on http://localhost:8080
make run-site
# Proxy all Open Match processes to view them.
make proxy
```

View File

@ -1 +0,0 @@
*"I notice that all the APIs use gRPC. What if I want to make my calls using REST, or via a Websocket?"** (gateway/proxy OSS projects are available)

26
docs/gcloud.md Normal file
View File

@ -0,0 +1,26 @@
# Create a GKE Cluster
Below are the steps to create a GKE cluster in Google Cloud Platform.
* Create a GCP project via [Google Cloud Console](https://console.cloud.google.com/).
* Billing must be enabled. If you're a new customer you can get some [free credits](https://cloud.google.com/free/).
* When you create a project you'll need to set a Project ID, if you forget it you can see it here, https://console.cloud.google.com/iam-admin/settings/project.
* Install [Google Cloud SDK](https://cloud.google.com/sdk/) which is the command line tool to work against your project.
Here are the next steps using the gcloud tool.
```bash
# Login to your Google Account for GCP
gcloud auth login
gcloud config set project $YOUR_GCP_PROJECT_ID
# Enable necessary GCP services
gcloud services enable containerregistry.googleapis.com
gcloud services enable container.googleapis.com
# Test that everything is good, this command should work.
gcloud compute zones list
# Create a GKE Cluster in this project
gcloud container clusters create --machine-type n1-standard-2 open-match-dev-cluster --zone us-west1-a --tags open-match
```

View File

@ -2,7 +2,7 @@
This is the {version} release of Open Match.
Check the [README](https://github.com/GoogleCloudPlatform/open-match/tree/release-{version}) for details on features, installation and usage.
Check the [README](https://github.com/googleforgames/open-match/tree/release-{version}) for details on features, installation and usage.
Release Notes
-------------
@ -18,7 +18,7 @@ Release Notes
**Security Fixes**
* Reduced privileges required for MMF. #PR
See [CHANGELOG](https://github.com/GoogleCloudPlatform/open-match/blob/release-{version}/CHANGELOG.md) for more details on changes.
See [CHANGELOG](https://github.com/googleforgames/open-match/blob/release-{version}/CHANGELOG.md) for more details on changes.
Images
------
@ -34,7 +34,7 @@ docker pull gcr.io/open-match-public-images/openmatch-mmlogicapi:{version}
docker pull gcr.io/open-match-public-images/openmatch-evaluator-serving:{version}
# Sample Match Making Functions
docker pull gcr.io/open-match-public-images/openmatch-mmf-go-grpc-serving-simple:{version}
docker pull gcr.io/open-match-public-images/openmatch-mmf-go-simple:{version}
# Test Clients
docker pull gcr.io/open-match-public-images/openmatch-backendclient:{version}
@ -55,7 +55,7 @@ kubectl create clusterrolebinding myname-cluster-admin-binding --clusterrole=clu
# Place all Open Match components in their own namespace.
kubectl create namespace open-match
# Install Open Match and monitoring services.
kubectl apply -f https://github.com/GoogleCloudPlatform/open-match/releases/download/v{version}/install.yaml --namespace open-match
# Install the example MMF and Evaluator.
kubectl apply -f https://github.com/GoogleCloudPlatform/open-match/releases/download/v{version}/install-example.yaml --namespace open-match
kubectl apply -f https://github.com/googleforgames/open-match/releases/download/v{version}/install.yaml --namespace open-match
# Install the demo.
kubectl apply -f https://github.com/googleforgames/open-match/releases/download/v{version}/install-demo.yaml --namespace open-match
```

View File

@ -12,16 +12,24 @@ SOURCE_VERSION=$1
DEST_VERSION=$2
SOURCE_PROJECT_ID=open-match-build
DEST_PROJECT_ID=open-match-public-images
IMAGE_NAMES="openmatch-backendapi openmatch-frontendapi openmatch-mmforc openmatch-mmlogicapi openmatch-evaluator-simple openmatch-mmf-cs-mmlogic-simple openmatch-mmf-go-mmlogic-simple openmatch-mmf-go-grpc-serving-simple openmatch-mmf-py3-mmlogic-simple openmatch-backendclient openmatch-clientloadgen openmatch-frontendclient"
IMAGE_NAMES="openmatch-backendapi openmatch-frontendapi openmatch-mmforc openmatch-mmlogicapi openmatch-evaluator-serving openmatch-mmf-go-simple openmatch-backendclient openmatch-clientloadgen openmatch-frontendclient"
for name in $IMAGE_NAMES
do
source_image=gcr.io/$SOURCE_PROJECT_ID/$name:$SOURCE_VERSION
dest_image=gcr.io/$DEST_PROJECT_ID/$name:$DEST_VERSION
dest_image_latest=gcr.io/$DEST_PROJECT_ID/$name:latest
docker pull $source_image
docker tag $source_image $dest_image
docker tag $source_image $dest_image_latest
docker push $dest_image
docker push $dest_image_latest
done
echo "=============================================================="
echo "=============================================================="
echo "=============================================================="
echo "=============================================================="
echo "Add these lines to your release notes:"
for name in $IMAGE_NAMES
do
echo "docker pull gcr.io/$DEST_PROJECT_ID/$name:$DEST_VERSION"
done

View File

@ -1,82 +0,0 @@
# Release {version}
<!--
This is the release issue template. Make a copy of the markdown in this page
and copy it into a release issue. Fill in relevent values, found inside {}
{version} should be replaced with the version ie: 0.5.0.
There are 3 types of releases:
* Release Candidates - 1.0.0-rc1
* Full Releases - 1.2.0
* Hot Fixes - 1.0.1
# Release Candidate and Full Release Process
1. Create a Release Issue from the [release issue template](./release_issue.md).
1. Label the issue `kind/release`, and attach it to the milestone that it matches.
1. Complete all items in the release issue checklist.
1. Close the release issue.
# Hot Fix Process
1. Hotfixes will occur as needed, to be determined by those will commit access on the repository.
1. Create a Release Issue from the [release issue template](./release_issue.md).
1. Label the issue `kind/release`, and attach it to the next upcoming milestone.
1. Complete all items in the release issue checklist.
1. Close the release issue.
!-->
Complete Milestone
------------------
- [ ] Create the next version milestone, use [semantic versioning](https://semver.org/) when naming it to be consistent with the [Go community](https://blog.golang.org/versioning-proposal).
- [ ] Visit the [milestone](https://github.com/GoogleCloudPlatform/open-match/milestone).
- [ ] Open a document for a draft [release notes](release.md).
- [ ] Add the milestone tag to all PRs and issues that were merged since the last milestone. Look at the [releases page](https://github.com/GoogleCloudPlatform/open-match/releases) and look for the "X commits to master since this release" for the diff. The link resolves to, https://github.com/GoogleCloudPlatform/open-match/compare/v{version}...master.
- [ ] Review all [milestone-less closed issues](https://github.com/GoogleCloudPlatform/open-match/issues?q=is%3Aissue+is%3Aclosed+no%3Amilestone) and assign the appropriate milestone.
- [ ] Review all [issues in milestone](https://github.com/GoogleCloudPlatform/open-match/milestones) for proper [labels](https://github.com/GoogleCloudPlatform/open-match/labels) (ex: area/build).
- [ ] Review all [milestone-less closed PRs](https://github.com/GoogleCloudPlatform/open-match/pulls?q=is%3Apr+is%3Aclosed+no%3Amilestone) and assign the appropriate milestone.
- [ ] Review all [PRs in milestone](https://github.com/GoogleCloudPlatform/open-match/milestones) for proper [labels](https://github.com/GoogleCloudPlatform/open-match/labels) (ex: area/build).
- [ ] View all open entries in milestone and move them to a future milestone if they aren't getting closed in time. https://github.com/GoogleCloudPlatform/open-match/milestones/v{version}
- [ ] Review all closed PRs against the milestone. Put the user visible changes into the release notes using the suggested format. https://github.com/GoogleCloudPlatform/open-match/pulls?q=is%3Apr+is%3Aclosed+milestone%3Av{version}
- [ ] Review all closed issues against the milestone. Put the user visible changes into the release notes using the suggested format. https://github.com/GoogleCloudPlatform/open-match/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aclosed+milestone%3Av{version}
- [ ] Verify the [milestone](https://github.com/GoogleCloudPlatform/open-match/milestones) is effectively 100% at this point with the exception of the release issue itself.
TODO: Add details for appropriate tagging for issues.
Build Artifacts
---------------
- [ ] Create a PR to bump the version.
- [ ] Open the [`Makefile`](makefile-version) and change BASE_VERSION value. Release candidates use the -rc# suffix.
- [ ] Open the [`install/helm/open-match/Chart.yaml`](om-chart-yaml-version) and [`install/helm/open-match-example/Chart.yaml`](om-example-chart-yaml-version) and change the `appVersion` and `version` entries.
- [ ] Open the [`install/helm/open-match/values.yaml`](om-values-yaml-version) and [`install/helm/open-match-example/values.yaml`](om-example-values-yaml-version) and change the `tag` entries.
- [ ] Open the [`site/config.toml`] and change the `release_branch` and `release_version` entries.
- [ ] Open the [`README.md`](readme-deploy) update the version references.
- [ ] Run `make clean release`
- [ ] There might be additional references to the old version but be careful not to change it for places that have it for historical purposes.
- [ ] Submit the pull request.
- [ ] Take note of the git hash in master, `git checkout master && git pull master && git rev-parse HEAD`
- [ ] Go to [Cloud Build](https://pantheon.corp.google.com/cloud-build/triggers?project=open-match-build), under Post Submit click "Run Trigger".
- [ ] Go to the History section and find the "Post Submit" build that's running. Wait for it to go Green. If it's red fix error repeat this section. Take note of version tag for next step.
- [ ] Run `./docs/governance/templates/release.sh {source version tag} {version}` to copy the images to open-match-public-images.
- [ ] Create a *draft* release with the [release template][release-template]
- [ ] Make a `tag` with the release version. The tag must be v{version}. Example: v0.5.0. Append -rc# for release candidates. Example: v0.5.0-rc1.
- [ ] Copy the files from `build/release/` generated from `make release` from earlier as release artifacts.
- [ ] Run `make delete-gke-cluster create-gke-cluster push-helm sleep-10 install-chart install-example-chart` and verify that the pods are all healthy.
- [ ] Run `make delete-gke-cluster create-gke-cluster` and run through the instructions under the [README](readme-deploy), verify the pods are healthy. You'll need to adjust the path to the `install/yaml/install.yaml` and `install/yaml/install-example.yaml` in your local clone since you haven't published them yet.
- [ ] Publish the [Release](om-release) in Github.
Announce
--------
- [ ] Send an email to the [mailing list](mailing-list-post) with the release details (copy-paste the release blog post)
- [ ] Send a chat on the [Slack channel](om-slack). "Open Match {version} has been released! Check it out at {release url}."
[om-slack]: https://open-match.slack.com/
[mailing-list-post]: https://groups.google.com/forum/#!newtopic/open-match-discuss
[release-template]: https://github.com/GoogleCloudPlatform/open-match/blob/master/docs/governance/templates/release.md
[makefile-version]: https://github.com/GoogleCloudPlatform/open-match/blob/master/Makefile#L53
[om-example-chart-yaml-version]: https://github.com/GoogleCloudPlatform/open-match/blob/master/install/helm/open-match/Chart.yaml#L16
[om-example-values-yaml-version]: https://github.com/GoogleCloudPlatform/open-match/blob/master/install/helm/open-match/values.yaml#L16
[om-example-chart-yaml-version]: https://github.com/GoogleCloudPlatform/open-match/blob/master/install/helm/open-match-example/Chart.yaml#L16
[om-example-values-yaml-version]: https://github.com/GoogleCloudPlatform/open-match/blob/master/install/helm/open-match-example/values.yaml#L16
[om-release]: https://github.com/GoogleCloudPlatform/open-match/releases/new
[readme-deploy]: https://github.com/GoogleCloudPlatform/open-match/blob/master/README.md#deploy-to-kubernetes

View File

@ -1,16 +1,16 @@
## Open Source Software integrations
### Structured logging
### Structured Logging - Logrus
Logging for Open Match uses the [Golang logrus module](https://github.com/sirupsen/logrus) to provide structured logs. Logs are output to `stdout` in each component, as expected by Docker and Kubernetes. Level and format are configurable via config/matchmaker_config.json. If you have a specific log aggregator as your final destination, we recommend you have a look at the logrus documentation as there is probably a log formatter that plays nicely with your stack.
### Instrumentation for metrics
### Instrumentation - OpenCensus
Open Match uses [OpenCensus](https://opencensus.io/) for metrics instrumentation. The [gRPC](https://grpc.io/) integrations are built-in, and Golang redigo module integrations are incoming, but [haven't been merged into the official repo](https://github.com/opencensus-integrations/redigo/pull/1). All of the core components expose HTTP `/metrics` endpoints on the port defined in `config/matchmaker_config.json` (default: 9555) for Prometheus to scrape. If you would like to export to a different metrics aggregation platform, we suggest you have a look at the OpenCensus documentation &mdash; there may be one written for you already, and switching to it may be as simple as changing a few lines of code.
**Note:** A standard for instrumentation of MMFs is planned.
### Redis setup
### State Storage - Redis
By default, Open Match expects you to run Redis *somewhere*. Connection information can be put in the config file (`matchmaker_config.json`) for any Redis instance reachable from the [Kubernetes namespace](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/). By default, Open Match sensibly runs in the Kubernetes `default` namespace. In most instances, we expect users will run a copy of Redis in a pod in Kubernetes, with a service pointing to it.

57
docs/knative.md Normal file
View File

@ -0,0 +1,57 @@
# Knative instructions
This describes the basic installation of knative (and istio) for serverless match functions.
It also provides experimental instructions for setting up you function host and endpoint.
## New cluster deploy instructions
Primarily taken from this [https://knative.dev/docs/install/] guide
### Install Istio
Download and apply the istio manifests
```
curl -L https://git.io/getLatestIstio | sh -
cd istio-1.0.5/
kubectl apply -f install/kubernetes/helm/istio/templates/crds.yaml
kubectl apply -f install/kubernetes/istio-demo-auth.yaml
```
Wait for `kubectl get pods --namespace istio-system` to complete
Don't forget, you'll need to apply istio-injection label for any namespace you deploy to
`kubectl label namespace default istio-injection=enabled`
### Install Knative
You may need to give your gcp user cluster admin privs to get through this part
`kubectl create clusterrolebinding cluster-admin-binding --clusterrole=cluster-admin --user="{your user}"`
To install knative, promethus + grafana (metrics), elk (logging), and zipkin (tracing), run. See the linked custom installation guide for more options
```
k apply -f https://github.com/knative/serving/releases/download/v0.3.0/serving.yaml /
-f https://github.com/knative/serving/releases/download/v0.3.0/monitoring.yaml
```
Wait for `kubectl get pods --namespace knative-serving` to complete
## Serving
To use the knative serverless flow for match functions...
1. Create a host-harness for running your function as a hosted service at `/api/function` (roadmap v0.4 and v0.6 for something more official)
2. Modify the provided knative serving manifest example at /deployments/k8s/knative_sample.yaml with your image name
3. Run `kubectl apply -f knative_sample.yaml` to start up an instance of the function on your knative installation
4. Using the configurable Http/1.1 REST pattern in open match (in review calebatwd/knative-rest-mmf), specify a `{..."hostName": "{knative service name}", "port": 8080}` in your match object properties when calling CreateMatch. The default port is 8080 in knative, but you can use whatever you like.
5. This should tell open match mmforc to call your function via hostname discovery in kubernetes over the knative ingress. You should see logs from your mmf function.
### Other Notes
- After 5 minutes of inactivity, knative will spin down your function. Subsequent calls will have a warm-up period of a few seconds depending on your function and host.
- The default port for knative is 8080, but you can use whatever you like so long as the intended port is Docker EXPOSED, knative will bind on that
- Dns discovery over knative has been problematic via the ingress. If you experience issues, considering updating the dns host discovery in mmforc to resolve on the full name `function-name.default.svc.cluster.local`
- Http2 is currently not supported in knative, so GRPC is not currently usable.
- The REST contract for the function can be found in cmd/mmforc/main.go as
```
type Profile struct {
JobName string
ProfId string
MoId string
PropId string
ResultsId string
Timestamp string
}
```

View File

@ -1,33 +0,0 @@
During alpha, please do not use Open Match as-is in production. To develop against it, please see the [development guide](development.md).
# "Productionizing" a deployment
Here are some steps that should be taken to productionize your Open Match deployment before exposing it to live public traffic. Some of these overlap with best practices for [productionizing Kubernetes](https://cloud.google.com/blog/products/gcp/exploring-container-security-running-a-tight-ship-with-kubernetes-engine-1-10) or cloud infrastructure more generally. We will work to make as many of these into the default deployment strategy for Open Match as possible, going forward.
**This is not an exhaustive list and addressing the items in this document alone shouldn't be considered sufficient. Every game is different and will have different production needs.**
## Kubernetes
All the usual guidance around hardening and securing Kubernetes are applicable to running Open Match. [Here is a guide around security for Google Kubernetes Enginge on GCP](https://cloud.google.com/blog/products/gcp/exploring-container-security-running-a-tight-ship-with-kubernetes-engine-1-10), and a number of other guides are available from reputable sources on the internet.
### Minimum permissions on Kubernetes
* The components of Open Match should be run in a separate Kubernetes namespace if you're also using the cluster for other services. As of 0.3.0 they run in the 'default' namespace if you follow the development guide.
* Note that the default MMForc process has cluster management permissions by default. Before moving to production, you should create a role with only access to create kubernetes jobs and configure the MMForc to use it.
### Kubernetes Jobs (MMFOrc)
The 0.3.0 MMFOrc component runs your MMFs as Kubernetes Jobs. You should periodically delete these jobs to keep the cluster running smoothly. How often you need to delete them is dependant on how many you are running. There are a number of open source solutions to do this for you. ***Note that once you delete the job, you won't have access to that job's logs anymore unless you're sending your logs from kubernetes to a log aggregator like Google Stackdriver. This can make it a challenge to troubleshoot issues***
### CPU and Memory limits
For any production Kubernetes deployment, it is good practice to profile your processes and select [resource limits and requests](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) according to your results. For example, you'll likely want to set adequate resource requests based on your expected player base and some load testing for the Redis state storage pods. This will help Kubernetes avoid scheduling other intensive processes on the same underlying node and keep you from running into resource contention issues. Another example might be an MMF with a particularly large memory or CPU footprint - maybe you have one that searches a lot of players for a potential match. This would be a good candidate for resource limits and requests in Kubernetes to both ensure it gets the CPU and RAM it needs to complete quickly, and to make sure it's not scheduled alongside another intensive Kubernetes pod.
### State storage
The default state storage for Open Match is a _single instance_ of Redis. Although it _is_ possible to go to production with this as the solution if you're willing to accept the potential downsides, for most deployments, a HA Redis configuration would better fit your needs. An example YAML file for creating a [self-healing HA Redis deployment on Kubernetes](../install/yaml/01-redis-failover.yaml) is available. Regardless of which configuation you use, it is probably a good idea to put some [resource requests](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) in your Kubernetes resource definition as mentioned above.
You can find more discussion in the [state storage readme doc](../internal/statestorage/redis/README.md).
## Open Match config
Debug logging and the extra debug code paths should be disabled in the `config/matchmaker_config.json` file (as of the time of this writing, 0.3.0).
## Public APIs for Open Match
In many cases, you may choose to configure your game clients to connect to the Open Match Frontend API, and in a few select cases (such as using it for P2P non-dedicated game server hosting), the game client may also need to connect to the Backend API. In these cases, it is important to secure the API endpoints against common attacks, such as DDoS or malformed packet floods.
* Using a cloud provider's Load Balancer in front of the Kubernetes Service is a common approach to enable vendor-specific DDoS protections. Check the documentation for your cloud vendor's Load Balancer for more details ([GCP's DDoS protection](https://cloud.google.com/armor/)).
* Using an API framework can be used to limit endpoint access to only game clients you have authenticated using your platform's authentication service. This may be accomplished with simple authentication tokens or a more complex scheme depending on your needs.
## Testing
(as of 0.3.0) The provided test programs are just for validating that Open Match is operating correctly; they are command-line applications designed to be run from within the same cluster as Open Match and are therefore not a suitable test harness for doing production testing to make sure your matchmaker is ready to handle your live game. Instead, it is recommended that you integrate Open Match into your game client and test it using the actual game flow players will use if at all possible.
### Load testing
Ideally, you would already be making 'headless' game clients for automated qa and load testing of your game servers; it is recommended that you also code these testing clients to be able to act as a mock player connecting to Open Match. Load testing platform services is a huge topic and should reflect your actual game access patterns as closely as possible, which will be very game dependant.
**Note: It is never a good idea to do load testing against a cloud vendor without informing them first!**

View File

@ -1,15 +1,10 @@
### Guides
* [Production guide](./docs/production.md) Lots of best practices to be written here before 1.0 release, right now it's a scattered collection of notes. **WIP**
* [Development guide](./docs/development.md)
## This all sounds great, but can you explain Docker and/or Kubernetes to me?
## Additional References
### Docker
- [Docker's official "Getting Started" guide](https://docs.docker.com/get-started/)
- [Katacoda's free, interactive Docker course](https://www.katacoda.com/courses/docker)
### Kubernetes
- [You should totally read this comic, and interactive tutorial](https://cloud.google.com/kubernetes-engine/kubernetes-comic/)
- [Katacoda's free, interactive Kubernetes course](https://www.katacoda.com/courses/kubernetes)
### Prometheus
- [Prometheus Operator spec](https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md)

View File

@ -1,60 +1,68 @@
# Roadmap. [Subject to change]
Releases are scheduled for every 6 weeks. **Every release is a stable, long-term-support version**. Even for alpha releases, best-effort support is available. With a little work and input from an experienced live services developer, you can go to production with any version on the [releases page](https://github.com/GoogleCloudPlatform/open-match/releases).
# Open Match Roadmap
Our current thinking is to wait to take Open Match out of alpha/beta (and label it 1.0) until it can be used out-of-the-box, standalone, for developers that dont have any existing platform services. Which is to say, the majority of **established game developers likely won't have any reason to wait for the 1.0 release if Open Match already handles your needs**. If you already have live platform services that you plan to integrate Open Match with (player authentication, a group invite system, dedicated game servers, metrics collection, logging aggregation, etc), then a lot of the features planned between 0.4.0 and 1.0 likely aren't of much interest to you anyway.
Open Match is currently at release 0.4.0. Open Match 0.5.0 currently has a Release Candidate and we are targeting to cut the release on 04/25/2019.
## Upcoming releases
* **0.4.0** &mdash; Agones Integration & MMF on [Knative](https://cloud.google.com/knative/)
MMF instrumentation
Match object expiration / lazy deletion
API autoscaling by default
API changes after this will likely be additions or very minor
* **0.5.0** &mdash; Tracing, Metrics, and KPI Dashboard
* **0.6.0** &mdash; Load testing suite
* **1.0.0** &mdash; API Formally Stable. Breaking API changes will require a new major version number.
* **1.1.0** &mdash; Canonical MMFs
Releases can be found on the [releases page](https://github.com/googleforgames/open-match/releases).
## Philosophy
* The next version (0.4.0) will focus on making MMFs run on serverless platforms - specifically Knative. This will just be first steps, as Knative is still pretty early. We want to get a proof of concept working so we can roadmap out the future "MMF on Knative" experience. Our intention is to keep MMFs as compatible as possible with the current Kubernetes job-based way of doing them. Our hope is that by the time Knative is mature, well be able to provide a [Knative build](https://github.com/Knative/build) pipeline that will take existing MMFs and build them as Knative functions. In the meantime, well map out a relatively painless (but not yet fully automated) way to make an existing MMF into a Kubernetes Deployment that looks as similar to what [Knative serving](https://github.com/knative/serving) is shaping up to be, in an effort to make the eventual switchover painless. Basically all of this is just _optimizing MMFs to make them spin up faster and take less resources_, **we're not planning to change what MMFs do or the interfaces they need to fulfill**. Existing MMFs will continue to run as-is, and in the future moving them to Knative should be both **optional** and **largely automated**.
* 0.4.0 represents the natural stopping point for adding new functionality until we have more community uptake and direction. We don't anticipate many API changes in 0.4.0 and beyond. Maybe new API calls for new functionality, but we're unlikely to see big shifts in existing calls through 1.0 and its point releases. We'll issue a new major release version if we decide we need those changes.
* The 0.5.0 version and beyond will be focused on operationalizing the out-of-the-box experience. Metrics and analytics and a default dashboard, additional tooling, and a load testing suite are all planned. We want it to be easy for operators to see KPI and know what's going on with Open Match.
Below sections detail the themes and the roadmap for the future releases. The tasks listed for the 0.6.0 release have been finalized and are well understood. As for the 0.7.0 and beyond, the tasks currently identified are listed. These are subject to change as we make our way through 0.6.0 release and get more feedback from the community.
# Planned improvements
See the [provisional roadmap](docs/roadmap.md) for more information on upcoming releases.
## 0.5.0 - Usability
## Documentation
- [ ] “Writing your first matchmaker” getting started guide will be included in an upcoming version.
- [ ] Documentation for using the example customizable components and the `backendstub` and `frontendstub` applications to do an end-to-end (e2e) test will be written. This all works now, but needs to be written up.
- [ ] Documentation on release process and release calendar.
The primary focus of the 0.5 release is usability. The goal for this release is to make Open Match easy to build and deploy and have solid supporting documentation. Users should be able to try Open Match 0.5.0 functionality and experiment with its features, MMFs etc. Here are some planned features for this release:
## State storage
- [X] All state storage operations should be isolated from core components into the `statestorage/` modules. This is necessary precursor work to enabling Open Match state storage to use software other than Redis.
- [X] [The Redis deployment should have an example HA configuration](https://github.com/GoogleCloudPlatform/open-match/issues/41)
- [X] Redis watch should be unified to watch a hash and stream updates. The code for this is written and validated but not committed yet.
- [ ] We don't want to support two redis watcher code paths, but we will until golang protobuf reflection is a bit more usable. [Design doc](https://docs.google.com/document/d/19kfhro7-CnBdFqFk7l4_HmwaH2JT_Rhw5-2FLWLEGGk/edit#heading=h.q3iwtwhfujjx), [github issue](https://github.com/golang/protobuf/issues/364)
- [X] Player/Group records generated when a client enters the matchmaking pool need to be removed after a certain amount of time with no activity. When using Redis, this will be implemented as a expiration on the player record.
- [X] Add support to invoke MMFs as a gRPC function call.
- [X] Provide a gRPC serving harness and an example MMF built using this harness. (golang based).
- [X] Provide a evaluation harness and a sample evaluator using this harness (golang based)
- [X] Deprecate the k8s based job scheduling mechanism for MMFs, Evaluator in favor of hosted MMFs, Evaluator.
- [X] Switch all core Open Match services to use gRPC style request / response protos.
- [X] Documentation: Add basic user, developer documentation and set up the Open Match website.
- [X] Create and document a formal release process.
- [X] Improve developer experience (simplify compiling, deploying and validating)
## Instrumentation / Metrics / Analytics
- [ ] Instrumentation of MMFs is in the planning stages. Since MMFs are by design meant to be completely customizable (to the point of allowing any process that can be packaged in a Docker container), metrics/stats will need to have an expected format and formalized outgoing pathway. Currently the thought is that it might be that the metrics should be written to a particular key in statestorage in a format compatible with opencensus, and will be collected, aggreggated, and exported to Prometheus using another process.
- [ ] [OpenCensus tracing](https://opencensus.io/core-concepts/tracing/) will be implemented in an upcoming version. This is likely going to require knative.
- [X] Read logrus logging configuration from matchmaker_config.json.
## 0.6.0 - API changes, Maturity
## Security
- [ ] The Kubernetes service account used by the MMFOrc should be updated to have min required permissions. [Issue 52](issues/52)
In 0.6.0 release, we are revisiting the Data Model and the API surface exposed by Open Match. The goal of this release is to front-load a major API refactoring that will facilitate achieving scale and other productionizing goals in forthcoming releases. Although breaking chagnes can happen any time till 1.0, the goal is to implement any major breaking changes in 0.6.0 so that future chagnes if any are relatively minor. Customer should be able to start building their Match Makers using the 0.6.0 API surface.
## Kubernetes
- [ ] Autoscaling isn't turned on for the Frontend or Backend API Kubernetes deployments by default.
- [X] A [Helm](https://helm.sh/) chart to stand up Open Match may be provided in an upcoming version. For now just use the [installation YAMLs](./install/yaml).
- [ ] A knative-based implementation of MMFs is in the planning stages.
Here are the tasks planned for 0.6.0 release:
## CI / CD / Build
- [X] We plan to host 'official' docker images for all release versions of the core components in publicly available docker registries soon. This is tracked in [Issue #45](issues/45) and is blocked by [Issue 42](issues/42).
- [X] CI/CD for this repo and the associated status tags are planned.
- [ ] Golang unit tests will be shipped in an upcoming version.
- [ ] A full load-testing and e2e testing suite will be included in an upcoming version.
- [ ] Implement the new Data model and the API changes for the Frontend, Backend and MMLogic API [Change Proposal](https://github.com/googleforgames/open-match/issues/279)
- [ ] Accept multiple proposals per MMF execution.
- [ ] Remove persistance of matches and proposals from Open Match state storage.
- [ ] Implement synchronized evaluation to eliminate use of state storage during evaluation.
- [ ] Introduce test framework for unit testing, Component testing and E2E testing.
- [ ] Add unit tests, component tests and integration tests for Open Match core components and examples.
- [ ] Update harness, evaluator, mmf samples etc., to reflect the API changes.
- [ ] Update documentation, website to reflect 0.6.0 API changes.
## Will not Implement
- [X] Defining multiple images inside a profile for the purposes of experimentation adds another layer of complexity into profiles that can instead be handled outside of open match with custom match functions in collaboration with a director (thing that calls backend to schedule matchmaking)
## 0.7.0 - Scale, Operationalizing
Features for 0.7.0 are targeted to enable Open Match to be productionizable. Note that as we identify more feature work past 0.6.0, these tasks may get pushed to future releases. However, these are core tasks that need to be addressed before Open Match reaches 1.0.
- [ ] Introduce Test framework for load, performance testing
- [ ] Automated Load / performance / scale tests
- [ ] Test results Dashboard
- [ ] Add support for Instrumentation, Monitoring, Dashboards
- [ ] Add support or Metrics collection, Analytics, Dashboards
- [ ] Identify Autoscaling patterns for each component and configure them.
## Other Features
Below are additional features that are not tied to a specific release but will be added incrementally across releases:
- [ ] Harness support for Python, PHP, C#, C++
- [ ] User Guide for Open Match, Tutorials
- [ ] Developer Guide for Open Match
- [ ] APIs & Reference
- [ ] Concept Documentation
- [ ] Website Improvements
## 1.1.0
Below are the features that have been identified but are not considered critical for Open Match (as a match making framework) to itself reach 1.0. Any other features that are related to Open Match ecosystem but not a part of the framework itself can be listed here. These features may not necessarily wait for Open Match 1.0 and can be implemented before that - but any of the currently identified 1.0 tasks are higher in priority than these to make Open Match production ready.
- [ ] Canonical usable examples out of box.
- [ ] KNative support to run MMFs
- [ ] OSS Director to integrate with Agones, other DGS backends
### Special Thanks
- Thanks to https://jbt.github.io/markdown-editor/ for help in marking this document down.

View File

@ -1,24 +0,0 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM open-match-base-build as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/examples/backendclient/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/examples/backendclient/backendclient .
COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/examples/backendclient/profiles profiles
ENTRYPOINT ["/backendclient"]

View File

@ -1,223 +0,0 @@
/*
Stubbed backend api client. This should be run within a k8s cluster, and
assumes that the backend api is up and can be accessed through a k8s service
named om-backendapi
Copyright 2018 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"io"
"io/ioutil"
"log"
"os"
"github.com/GoogleCloudPlatform/open-match/internal/pb"
"github.com/gobs/pretty"
"github.com/tidwall/gjson"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)
var (
filename = flag.String("file", "profiles/testprofile.json", "JSON file from which to read match properties")
beCall = flag.String("call", "ListMatches", "Open Match backend match request gRPC call to test")
server = flag.String("backend", "om-backendapi:50505", "Hostname and IP of the Open Match backend")
assignment = flag.String("assignment", "example.server.dgs:12345", "Assignment to send to matched players")
delAssignments = flag.Bool("rm", false, "Delete assignments. Leave off to be able to manually validate assignments in state storage")
verbose = flag.Bool("verbose", false, "Print out as much as possible")
)
func bytesToString(data []byte) string {
return string(data[:])
}
func ppJSON(s string) {
if *verbose {
buf := new(bytes.Buffer)
json.Indent(buf, []byte(s), "", " ")
log.Println(buf)
}
return
}
func main() {
flag.Parse()
log.Print("Parsing flags:")
log.Printf(" [flags] Reading properties from file at %v", *filename)
log.Printf(" [flags] Using OM Backend address %v", *server)
log.Printf(" [flags] Using OM Backend %v call", *beCall)
log.Printf(" [flags] Assigning players to %v", *assignment)
log.Printf(" [flags] Deleting assignments? %v", *delAssignments)
if !(*beCall == "CreateMatch" || *beCall == "ListMatches") {
log.Printf(" [flags] Unknown OM Backend call %v! Exiting...", *beCall)
return
}
// Read the profile
jsonFile, err := os.Open(*filename)
if err != nil {
log.Fatalf("Failed to open file %v", *filename)
}
defer jsonFile.Close()
// parse json data and remove extra whitespace before sending to the backend.
jsonData, _ := ioutil.ReadAll(jsonFile) // this reads as a byte array
buffer := new(bytes.Buffer) // convert byte array to buffer to send to json.Compact()
if err := json.Compact(buffer, jsonData); err != nil {
log.Println(err)
}
jsonProfile := buffer.String()
pbProfile := &pb.MatchObject{}
pbProfile.Properties = jsonProfile
conn, err := grpc.Dial(*server, grpc.WithInsecure())
if err != nil {
log.Fatalf("failed to connect: %s", err.Error())
}
client := pb.NewBackendClient(conn)
log.Println("Backend client connected to", *server)
var profileName string
if gjson.Get(jsonProfile, "name").Exists() {
profileName = gjson.Get(jsonProfile, "name").String()
} else {
profileName = "testprofilename"
log.Println("JSON Profile does not contain a name; using ", profileName)
}
pbProfile.Id = profileName
pbProfile.Properties = jsonProfile
mmfcfg := &pb.MmfConfig{Name: "profileName"}
mmfcfg.Type = pb.MmfConfig_GRPC
mmfcfg.Host = gjson.Get(jsonProfile, "hostname").String()
mmfcfg.Port = int32(gjson.Get(jsonProfile, "port").Int())
req := &pb.CreateMatchRequest{
Match: pbProfile,
Mmfcfg: mmfcfg,
}
log.Println("Backend Request:")
ppJSON(jsonProfile)
pretty.PrettyPrint(mmfcfg)
log.Printf("Establishing HTTPv2 stream...")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
matchChan := make(chan *pb.MatchObject)
doneChan := make(chan bool)
go func() {
// Watch for results and print as they come in.
log.Println("Watching for match results...")
for {
select {
case match := <-matchChan:
if match.Error == "insufficient players" {
log.Println("Waiting for a larger player pool...")
}
// Validate JSON before trying to parse it
if !gjson.Valid(string(match.Properties)) {
log.Println(errors.New("invalid json"))
}
log.Println("Received match:")
pretty.PrettyPrint(match)
// Assign players in this match to our server
log.Println("Assigning players to DGS at", *assignment)
assign := &pb.Assignments{Rosters: match.Rosters, Assignment: *assignment}
log.Printf("Waiting for matches...")
_, err = client.CreateAssignments(context.Background(), &pb.CreateAssignmentsRequest{
Assignment: assign,
})
if err != nil {
log.Println(err)
}
log.Println("Success!")
if *delAssignments {
log.Println("deleting assignments")
for _, a := range assign.Rosters {
_, err = client.DeleteAssignments(context.Background(), &pb.DeleteAssignmentsRequest{Roster: a})
if err != nil {
log.Println(err)
}
log.Println("Success Deleting Assignments!")
}
} else {
log.Println("Not deleting assignments [demo mode].")
}
}
if *beCall == "CreateMatch" {
// Got a result; done here.
log.Println("Got single result from CreateMatch, exiting...")
doneChan <- true
return
}
}
}()
// Make the requested backend call: CreateMatch calls once, ListMatches continually calls.
log.Printf("Attempting %v() call", *beCall)
switch *beCall {
case "CreateMatch":
resp, err := client.CreateMatch(ctx, req)
if err != nil {
panic(err)
}
log.Printf("CreateMatch returned; processing match")
matchChan <- resp.Match
<-doneChan
case "ListMatches":
stream, err := client.ListMatches(ctx, &pb.ListMatchesRequest{
Mmfcfg: req.Mmfcfg,
Match: req.Match,
})
if err != nil {
log.Fatalf("Attempting to open stream for ListMatches(_) = _, %v", err)
}
for {
log.Printf("Waiting for matches...")
resp, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
stat, ok := status.FromError(err)
if ok {
log.Printf("Error reading stream for ListMatches() returned status: %s %s", stat.Code().String(), stat.Message())
} else {
log.Printf("Error reading stream for ListMatches() returned status: %s", err)
}
break
}
matchChan <- resp.Match
}
}
}

View File

@ -1,53 +0,0 @@
{
"name":"testprofilev1",
"id":"testprofile",
"hostname": "om-function",
"port": 50502,
"properties":{
"pools": [
{
"name": "defaultPool",
"filters": [
{ "name": "europeWest1ElapsedUnder150", "attribute": "region.europe-west1", "maxv": "150" },
{ "name": "silverRanking", "attribute": "mmr.rating", "maxv": "1250", "minv": "950" }
]
},
{
"name": "supportPool",
"filters": [
{ "name": "europeWest1ElapsedUnder150", "attribute": "region.europe-west1", "maxv": "150" },
{ "name": "silverRanking", "attribute": "mmr.rating", "maxv": "1250", "minv": "950" },
{ "name": "supportRole", "attribute": "role.support", "maxv": "2147483647" }
]
}
],
"rosters": [
{
"name": "red",
"players": [
{ "pool": "defaultPool" },
{ "pool": "defaultPool" },
{ "pool": "defaultPool" },
{ "pool": "defaultPool" },
{ "pool": "defaultPool" },
{ "pool": "defaultPool" },
{ "pool": "defaultPool" },
{ "pool": "defaultPool" }
]
},
{
"name": "blu",
"players": [
{ "pool": "defaultPool" },
{ "pool": "defaultPool" },
{ "pool": "defaultPool" },
{ "pool": "defaultPool" },
{ "pool": "defaultPool" },
{ "pool": "defaultPool" },
{ "pool": "defaultPool" },
{ "pool": "defaultPool" }
]
}
]
}
}

View File

@ -1,23 +0,0 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM open-match-base-build as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/examples/evaluators/golang/serving/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/examples/evaluators/golang/serving/serving .
ENTRYPOINT ["/serving"]

View File

@ -1,125 +0,0 @@
/*
This is a sample Evaluator built using the Evaluator Harness. It evaluates
multiple proposals and approves a subset of them. This sample demonstrates
how to build a basic Evaluator using the Evaluator Harness . This example
over-simplifies the actual evaluation decisions and hence should not be
used as is for a real scenario.
Copyright 2018 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"fmt"
harness "github.com/GoogleCloudPlatform/open-match/internal/harness/evaluator/golang"
"github.com/GoogleCloudPlatform/open-match/internal/pb"
)
func main() {
// This invoke the harness to set up the Evaluator. The harness abstracts
// fetching proposals ready for evaluation and the process of transforming
// approved proposals into results that Open Match can relay to the caller
// requesting for matches.
harness.RunEvaluator(Evaluate)
}
// Evaluate is where your custom evaluation logic lives.
// Input:
// - proposals : List of all the proposals to be consiered for evaluation. Each proposal will have
// Rosters comprising of the players belonging to that proposal.
// Output:
// - (proposals) : List of approved proposal IDs that can be returned as match results.
func Evaluate(ctx context.Context, proposals []*pb.MatchObject) ([]string, error) {
// Map of approved and overloaded proposals. Using maps for easier lookup.
approvedProposals := map[string]bool{}
overloadedProposals := map[string]bool{}
// Map of all the players encountered in the proposals. Each entry maps a player id to
// the first match in which the player was encountered.
allPlayers := map[string]string{}
// Iterate over each proposal to either add to approved map or overloaded map.
for _, proposal := range proposals {
proposalID := proposal.Id
approved := true
players := getPlayersInProposal(proposal)
// Iterate over each player in the proposal to check if the player was encountered before.
for _, playerID := range players {
if propID, found := allPlayers[playerID]; found {
// Player was encountered in an earlier proposal. Mark the current proposal as overloaded (not approved).
// Also, the first proposal where the player was encountered may have been marked approved. Remove that proposal
// approved proposals and add to overloaded proposals since we encountered its player in current proposal too.
approved = false
delete(approvedProposals, propID)
overloadedProposals[propID] = true
} else {
// Player encountered for the first time, add to all players map with the current proposal.
allPlayers[playerID] = proposalID
}
if approved {
approvedProposals[proposalID] = true
} else {
overloadedProposals[proposalID] = true
}
}
}
// Convert the maps to lists of overloaded, approved proposals.
overloadedList := []string{}
approvedList := []string{}
for k := range overloadedProposals {
overloadedList = append(overloadedList, k)
}
for k := range approvedProposals {
approvedList = append(approvedList, k)
}
// Select proposals to approve from the overloaded proposals list.
chosen, err := chooseProposals(overloadedList)
if err != nil {
return nil, fmt.Errorf("Failed to select approved list from overloaded proposals, %v", err)
}
// Add the chosen proposals to the approved list.
approvedList = append(approvedList, chosen...)
return approvedList, nil
}
// chooseProposals should look through all overloaded proposals (that is, have a player that is also
// in another proposed match) and choose the proposals to approve. This is where the core evaluation
// logic will be added.
func chooseProposals(overloaded []string) ([]string, error) {
// As a basic example, we pick the first overloaded proposal for approval.
approved := []string{}
if len(overloaded) > 0 {
approved = append(approved, overloaded[0])
}
return approved, nil
}
func getPlayersInProposal(proposal *pb.MatchObject) []string {
var players []string
for _, r := range proposal.Rosters {
for _, p := range r.Players {
players = append(players, p.Id)
}
}
return players
}

View File

@ -1,136 +0,0 @@
/*
This is a sample match function that uses the GRPC harness to set up
the match making function as a service. This sample is a reference
to demonstrate the usage of the GRPC harness and should only be used as
a starting point for your match function. You will need to modify the
matchmaking logic in this function based on your game's requirements.
Copyright 2018 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"errors"
"math/rand"
"time"
harness "github.com/GoogleCloudPlatform/open-match/internal/harness/matchfunction/golang"
"github.com/GoogleCloudPlatform/open-match/internal/pb"
log "github.com/sirupsen/logrus"
)
func main() {
// Invoke the harness to setup a GRPC service that handles requests to run the
// match function. The harness itself queries open match for player pools for
// the specified request and passes the pools to the match function to generate
// proposals.
harness.ServeMatchFunction(&harness.HarnessParams{
FunctionName: "simple-matchfunction",
ServicePortConfigName: "api.functions.port",
ProxyPortConfigName: "api.functions.proxyport",
Func: makeMatches,
})
}
// makeMatches is where your custom matchmaking logic lives.
// Input:
// - profile : 'Properties' of a MatchObject specified in the ListMatches or CreateMatch call.
// - rosters : An array of Rosters. By convention, your input Roster contains players already in
// the match, and the names of pools to search when trying to fill an empty slot.
// - pools : An array PlayerPool messages. Contains all the players returned by the MMLogic API
// upon querying for the player pools. It already had ignorelists applied as of the
// time the player pool was queried.
// Output:
// - (results) JSON blob to populated in the MatchObject 'Properties' field sent to the ListMatches
// or CreateMatch call.
// - (rosters) Populated team rosters. Use is optional but recommended;
// you'll need to construct at least one Roster with all the players you're selecting
// as it is used to add those players to the ignorelist. You'll also need all
// the players you want to assign to your DGS in Roster(s) when you call the
// BackendAPI CreateAssignments() endpoint. Might as well put them in rosters now.
// - error : Use if you need to return an unrecoverable error.
func makeMatches(ctx context.Context, logger *log.Entry, profile string, rosters []*pb.Roster, pools []*pb.PlayerPool) (string, []*pb.Roster, error) {
// Open Match will try to marshal your JSON roster to an array of protobuf Roster objects. It's
// up to you if you want to fill these protobuf Roster objects or just write your Rosters in your
// custom JSON blob. This example uses the protobuf Rosters.
// Used for tracking metrics.
var selectedPlayerCount int64
// Loop through all the team rosters sent in the call to create a match.
for ti, team := range rosters {
logger.Infof(" Attempting to fill team: %v", team.Name)
// Loop through all the players slots on this team roster.
for si, slot := range team.Players {
// Loop through all the pools and check if there is a pool with a matching name to the
// poolName for this player slot. Just one example of a way for your matchmaker to
// specify which pools your MMF should search through to fill a given player slot.
// Optional, feel free to change as you see fit.
for _, pool := range pools {
if slot.Pool == pool.Name && len(pool.Roster.Players) > 0 {
/////////////////////////////////////////////////////////
// These next few lines are where you would put your custom logic, such as
// searching the pool for players with similar latencies or skill ratings
// to the players you have already selected. This example doesn't do anything
// but choose at random!
logger.Infof("Looking for player in pool: %v, size: %v", pool.Name, len(pool.Roster.Players))
randPlayerIndex := rand.New(rand.NewSource(time.Now().UnixNano())).Intn(len(pool.Roster.Players))
// Get random player with this index
selectedPlayer := pool.Roster.Players[randPlayerIndex]
logger.Infof("Selected player index %v: %v", randPlayerIndex, selectedPlayer.Id)
// Remove this player from the array as they are now used up.
pool.Roster.Players[randPlayerIndex] = pool.Roster.Players[0]
// This is a functional pop from a set.
_, pool.Roster.Players = pool.Roster.Players[0], pool.Roster.Players[1:]
// Write the player to the slot and loop.
rosters[ti].Players[si] = selectedPlayer
selectedPlayerCount++
break
/////////////////////////////////////////////////////////
} else {
// Weren't enough players left in the pool to fill all the slots so this example errors out.
// For this example, this is an error condition and so the match result will have the error
// populated. If your game can handle partial results, customize this to NOT return an error
// and instaead populate the result with any properties that may be needed to evaluate the proposal.
return "", rosters, errors.New("insufficient players")
}
}
}
}
logger.Info(" Rosters complete.")
// You can send back any arbitrary JSON in the first return value (the 'results' string). It
// will get sent back out the backend API in the Properties field of the MatchObject message.
// In this example, all the selected players are populated to the Rosters array, so we'll just
// pass back the input profile back as the results. If there was anything else arbitrary as
// output from the MMF, it could easily be included here.
results := profile
logger.Infof("Selected %v players", selectedPlayerCount)
return results, rosters, nil
}

View File

@ -14,10 +14,10 @@
FROM open-match-base-build as builder
WORKDIR /go/src/github.com/GoogleCloudPlatform/open-match/examples/functions/golang/grpc-serving
WORKDIR /go/src/open-match.dev/open-match/examples/functions/golang/simple
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o matchfunction .
FROM gcr.io/distroless/static
COPY --from=builder /go/src/github.com/GoogleCloudPlatform/open-match/examples/functions/golang/grpc-serving/matchfunction .
COPY --from=builder /go/src/open-match.dev/open-match/examples/functions/golang/simple/matchfunction .
ENTRYPOINT ["/matchfunction"]

View File

@ -0,0 +1,79 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package main a sample match function that uses the GRPC harness to set up
// the match making function as a service. This sample is a reference
// to demonstrate the usage of the GRPC harness and should only be used as
// a starting point for your match function. You will need to modify the
// matchmaking logic in this function based on your game's requirements.
package main
import (
"fmt"
"time"
goHarness "open-match.dev/open-match/internal/harness/golang"
"open-match.dev/open-match/internal/pb"
)
func main() {
// Invoke the harness to setup a GRPC service that handles requests to run the
// match function. The harness itself queries open match for player pools for
// the specified request and passes the pools to the match function to generate
// proposals.
goHarness.RunMatchFunction(&goHarness.FunctionSettings{
FunctionName: "simple-matchfunction",
Func: makeMatches,
})
}
// makeMatches is where your custom matchmaking logic lives.
func makeMatches(p *goHarness.MatchFunctionParams) []*pb.Match {
// This simple match function does the following things
// 1. Deduplicates the tickets from the pools into a single list.
// 2. Groups players into 1v1 matches.
tickets := map[string]*pb.Ticket{}
for _, pool := range p.PoolNameToTickets {
for _, ticket := range pool {
tickets[ticket.Id] = ticket
}
}
var matches []*pb.Match
t := time.Now().Format("2006-01-02T15:04:05.00")
thisMatch := make([]*pb.Ticket, 0, 2)
matchNum := 0
for _, ticket := range tickets {
thisMatch = append(thisMatch, ticket)
if len(thisMatch) >= 2 {
matches = append(matches, &pb.Match{
MatchId: fmt.Sprintf("profile-%s-time-%s-num-%d", p.ProfileName, t, matchNum),
MatchProfile: p.ProfileName,
MatchFunction: "a-simple-matchfunction",
Ticket: thisMatch,
})
thisMatch = make([]*pb.Ticket, 0, 2)
matchNum++
}
}
return matches
}

62
go.mod
View File

@ -1,57 +1,45 @@
module github.com/GoogleCloudPlatform/open-match
module open-match.dev/open-match
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
go 1.12
require (
cloud.google.com/go v0.37.4 // indirect
contrib.go.opencensus.io/exporter/jaeger v0.1.0
contrib.go.opencensus.io/exporter/prometheus v0.1.0
contrib.go.opencensus.io/exporter/stackdriver v0.11.0
contrib.go.opencensus.io/exporter/zipkin v0.1.1
github.com/TV4/logrus-stackdriver-formatter v0.1.0
github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 // indirect
github.com/alicebob/miniredis v2.5.0+incompatible
github.com/aws/aws-sdk-go v1.19.25 // indirect
github.com/cenkalti/backoff v2.1.1+incompatible
github.com/fsnotify/fsnotify v1.4.7
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b
github.com/gogo/protobuf v1.2.1
github.com/golang/protobuf v1.3.1
github.com/gomodule/redigo v1.7.0
github.com/google/btree v1.0.0 // indirect
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/googleapis/gnostic v0.2.0 // indirect
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
github.com/grpc-ecosystem/grpc-gateway v1.8.5
github.com/hashicorp/golang-lru v0.5.1 // indirect
github.com/json-iterator/go v1.1.6 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/opencensus-integrations/redigo v2.0.1+incompatible
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/openzipkin/zipkin-go v0.1.6
github.com/pkg/errors v0.8.0
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 // indirect
github.com/prometheus/procfs v0.0.0-20190403104016-ea9eea638872 // indirect
github.com/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829
github.com/rs/xid v1.2.1
github.com/sirupsen/logrus v1.4.1
github.com/spf13/viper v1.3.2
github.com/stretchr/testify v1.3.0 // indirect
github.com/tidwall/gjson v1.2.1
github.com/tidwall/match v1.0.1 // indirect
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51 // indirect
github.com/tidwall/sjson v1.0.4
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583 // indirect
go.opencensus.io v0.20.2
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 // indirect
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a // indirect
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67 // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
google.golang.org/appengine v1.5.0
github.com/stretchr/testify v1.3.0
github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036 // indirect
go.opencensus.io v0.21.0
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107
google.golang.org/grpc v1.20.0
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.2.2
k8s.io/api v0.0.0-20190222213804-5cb15d344471
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628
k8s.io/client-go v10.0.0+incompatible
k8s.io/klog v0.2.0 // indirect
sigs.k8s.io/yaml v1.1.0 // indirect
)

Some files were not shown because too many files have changed in this diff Show More