├── gradle.properties ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── docker ├── entrypoint.sh └── Dockerfile ├── charts └── orbit-carnival │ ├── templates │ ├── monitoring-namespace.yaml │ ├── ingress.yaml │ ├── service.yaml │ ├── grafana-service.yaml │ ├── prometheus-service.yaml │ ├── grafana-datasource-config.yaml │ ├── deployment.yaml │ ├── prometheus-cluster-role.yaml │ ├── prometheus-deployment.yaml │ ├── grafana-deployment.yaml │ ├── _helpers.tpl │ └── prometheus-config.yaml │ ├── Chart.yaml │ └── values.yaml ├── skaffold.yaml ├── src └── main │ ├── kotlin │ └── orbit │ │ └── carnival │ │ ├── actors │ │ ├── repository │ │ │ ├── PlayerStore.kt │ │ │ ├── GameStore.kt │ │ │ ├── local │ │ │ │ ├── LocalGameStore.kt │ │ │ │ └── LocalPlayerStore.kt │ │ │ └── etcd │ │ │ │ ├── EtcdGameStore.kt │ │ │ │ └── EtcdPlayerStore.kt │ │ ├── Player.kt │ │ └── Game.kt │ │ ├── Carnival.kt │ │ ├── Server.kt │ │ ├── LoadServer.kt │ │ └── App.kt │ └── resources │ └── games.yml ├── .gitignore ├── docker-compose.yaml ├── .github └── workflows │ └── release.yml ├── Orbit-Carnival.postman_collection.json ├── gradlew.bat ├── README.md ├── gradlew ├── Orbit-Carnival.insomnia_collection.json └── grafana-dashboard.json /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'orbit-carnival' 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orbit/orbit-sample/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Start Orbit Carnival sample app 4 | cd /opt/orbitCarnival 5 | java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 ./libs/orbit-carnival.jar -------------------------------------------------------------------------------- /charts/orbit-carnival/templates/monitoring-namespace.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.metrics.enabled }} 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: monitoring 6 | labels: 7 | name: monitoring 8 | {{- end }} 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /charts/orbit-carnival/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: orbit-carnival-ingress 5 | labels: 6 | app: orbit-carnival 7 | spec: 8 | backend: 9 | serviceName: orbit-carnival 10 | servicePort: 8001 -------------------------------------------------------------------------------- /charts/orbit-carnival/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: orbit-carnival 5 | labels: 6 | app: orbit-carnival 7 | spec: 8 | selector: 9 | app: orbit-carnival 10 | ports: 11 | - name: http 12 | port: 8001 13 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM azul/zulu-openjdk-alpine:11 2 | 3 | ADD build/libs/orbit-carnival-release.jar /opt/orbitCarnival/libs/orbit-carnival.jar 4 | ADD docker/entrypoint.sh /opt/orbitCarnival/ 5 | 6 | RUN ["chmod", "+x", "/opt/orbitCarnival/entrypoint.sh"] 7 | 8 | CMD ["/opt/orbitCarnival/entrypoint.sh"] -------------------------------------------------------------------------------- /charts/orbit-carnival/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | appVersion: "1.0" 3 | description: An Orbit Sample app 4 | name: orbit-carnival 5 | type: application 6 | version: 0.1.0 7 | dependencies: 8 | - name: orbit 9 | version: 2.0.0-alpha.100 10 | repository: https://www.orbit.cloud/orbit 11 | condition: orbit.enabled 12 | -------------------------------------------------------------------------------- /charts/orbit-carnival/templates/grafana-service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.metrics.enabled }} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: grafana 6 | namespace: monitoring 7 | annotations: 8 | prometheus.io/scrape: 'true' 9 | prometheus.io/port: '3000' 10 | spec: 11 | selector: 12 | app: grafana 13 | type: NodePort 14 | ports: 15 | - port: 3000 16 | targetPort: 3000 17 | nodePort: 32000 18 | {{- end }} 19 | -------------------------------------------------------------------------------- /charts/orbit-carnival/templates/prometheus-service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.metrics.enabled }} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: prometheus-service 6 | namespace: monitoring 7 | annotations: 8 | prometheus.io/scrape: 'true' 9 | prometheus.io/port: '9090' 10 | 11 | spec: 12 | selector: 13 | app: prometheus-server 14 | type: NodePort 15 | ports: 16 | - port: 8080 17 | targetPort: 9090 18 | nodePort: 30000 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta2 2 | kind: Config 3 | metadata: 4 | name: orbit-carnival 5 | build: 6 | artifacts: 7 | - image: orbit-carnival 8 | context: . 9 | docker: 10 | dockerfile: docker/Dockerfile 11 | deploy: 12 | helm: 13 | releases: 14 | - name: orbit-carnival 15 | chartPath: charts/orbit-carnival 16 | skipBuildDependencies: true 17 | setValues: 18 | url: orbit-carnival 19 | orbit.enabled: true 20 | values: 21 | image: orbit-carnival 22 | -------------------------------------------------------------------------------- /src/main/kotlin/orbit/carnival/actors/repository/PlayerStore.kt: -------------------------------------------------------------------------------- 1 | package orbit.carnival.actors.repository 2 | 3 | import orbit.carnival.actors.PlayerImpl 4 | 5 | interface PlayerStore { 6 | suspend fun get(): List 7 | suspend fun get(id: String): PlayerRecord? 8 | suspend fun put(player: PlayerRecord) 9 | } 10 | 11 | data class PlayerRecord(val id: String, val rewards: List) 12 | 13 | fun PlayerImpl.toRecord(): PlayerRecord { 14 | return PlayerRecord(this.id, rewards = this.rewards) 15 | } 16 | -------------------------------------------------------------------------------- /src/main/kotlin/orbit/carnival/actors/repository/GameStore.kt: -------------------------------------------------------------------------------- 1 | package orbit.carnival.actors.repository 2 | 3 | import orbit.carnival.actors.GameImpl 4 | import orbit.carnival.actors.PlayedGameResult 5 | 6 | interface GameStore { 7 | suspend fun get(): List 8 | suspend fun get(gameId: String): GameRecord? 9 | suspend fun put(game: GameRecord) 10 | } 11 | 12 | data class GameRecord(val id: String, val results: List) 13 | 14 | fun GameImpl.toRecord() : GameRecord { 15 | return GameRecord(this.id, results = this.results) 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/main/kotlin/orbit/carnival/actors/repository/local/LocalGameStore.kt: -------------------------------------------------------------------------------- 1 | package orbit.carnival.actors.repository.local 2 | 3 | import orbit.carnival.actors.repository.GameRecord 4 | import orbit.carnival.actors.repository.GameStore 5 | 6 | class LocalGameStore : GameStore { 7 | private val store = mutableMapOf() 8 | 9 | suspend override fun get(): List { 10 | return store.values.toList() 11 | } 12 | 13 | suspend override fun get(gameId: String): GameRecord? { 14 | return store[gameId] 15 | } 16 | 17 | suspend override fun put(game: GameRecord) { 18 | store[game.id] = game 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/orbit/carnival/actors/repository/local/LocalPlayerStore.kt: -------------------------------------------------------------------------------- 1 | package orbit.carnival.actors.repository.local 2 | 3 | import orbit.carnival.actors.repository.PlayerRecord 4 | import orbit.carnival.actors.repository.PlayerStore 5 | 6 | class LocalPlayerStore : PlayerStore { 7 | private val store = mutableMapOf() 8 | 9 | override suspend fun get(): List { 10 | return store.values.toList() 11 | } 12 | 13 | override suspend fun get(id: String): PlayerRecord? { 14 | return store[id] 15 | } 16 | 17 | override suspend fun put(player: PlayerRecord) { 18 | store[player.id] = player 19 | } 20 | } -------------------------------------------------------------------------------- /charts/orbit-carnival/templates/grafana-datasource-config.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.metrics.enabled }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: grafana-datasources 6 | namespace: monitoring 7 | data: 8 | prometheus.yaml: |- 9 | { 10 | "apiVersion": 1, 11 | "datasources": [ 12 | { 13 | "access":"proxy", 14 | "default": true, 15 | "editable": true, 16 | "name": "prometheus", 17 | "orgId": 1, 18 | "type": "prometheus", 19 | "url": "http://prometheus-service:8080", 20 | "version": 1 21 | } 22 | ] 23 | } 24 | {{- end }} -------------------------------------------------------------------------------- /charts/orbit-carnival/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: orbit-carnival 5 | labels: 6 | app: orbit-carnival 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: orbit-carnival 12 | template: 13 | metadata: 14 | labels: 15 | app: orbit-carnival 16 | spec: 17 | containers: 18 | - name: orbit-carnival 19 | image: {{ .Values.image }} 20 | ports: 21 | - name: http 22 | containerPort: 8001 23 | - name: debug 24 | containerPort: 5005 25 | env: 26 | - name: ORBIT_URL 27 | value: {{ .Values.orbitUrl }} -------------------------------------------------------------------------------- /charts/orbit-carnival/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for charts. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | imagePullSecrets: [] 6 | nameOverride: "" 7 | fullnameOverride: "" 8 | 9 | image: "orbitframework/orbit-sample" 10 | orbitUrl: "dns:///orbit:50056/" 11 | 12 | resources: {} 13 | 14 | meters: 15 | enabled: true 16 | 17 | orbit: 18 | enabled: true 19 | url: localhost 20 | fullnameOverride: "orbit" 21 | node: 22 | replicas: 1 23 | containerPort: 50056 24 | service: 25 | annotations: 26 | prometheus.io/scrape: 'true' 27 | prometheus.io/port: '9090' 28 | addressableDirectory: 29 | replicas: 1 30 | nodeDirectory: 31 | replicas: 1 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven 2 | *.versionsBackup 3 | dependency-reduced-pom.xml 4 | target/ 5 | 6 | # Gradle 7 | .gradle 8 | out/ 9 | build/ 10 | 11 | # IntelliJ 12 | *.iml 13 | .idea/* 14 | !.idea/copyright/ 15 | !.idea/codeStyles/ 16 | !.idea/codeStyleSettings.xml 17 | 18 | # Eclipse 19 | .project/ 20 | .classpath/ 21 | .settings/ 22 | .factorypath/ 23 | .apt_generated/ 24 | 25 | # VSCode 26 | .project 27 | .classpath 28 | bin/ 29 | 30 | # Netbeans 31 | nbproject/private/ 32 | nbbuild/ 33 | dist/ 34 | nbdist/ 35 | .nb-gradle/ 36 | 37 | # Logging 38 | **/log/** 39 | 40 | # Docs 41 | /site/ 42 | 43 | # Mac 44 | .DS_Store 45 | 46 | # Windows 47 | thumbs.db 48 | Desktop.ini 49 | $RECYCLE.BIN/ 50 | charts/orbit-carnival/charts 51 | 52 | #IDEA 53 | .idea/ 54 | -------------------------------------------------------------------------------- /charts/orbit-carnival/templates/prometheus-cluster-role.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.metrics.enabled }} 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | kind: ClusterRole 4 | metadata: 5 | name: prometheus 6 | rules: 7 | - apiGroups: [""] 8 | resources: 9 | - nodes 10 | - nodes/proxy 11 | - services 12 | - endpoints 13 | - pods 14 | verbs: ["get", "list", "watch"] 15 | - apiGroups: 16 | - extensions 17 | resources: 18 | - ingresses 19 | verbs: ["get", "list", "watch"] 20 | - nonResourceURLs: ["/metrics"] 21 | verbs: ["get"] 22 | --- 23 | apiVersion: rbac.authorization.k8s.io/v1beta1 24 | kind: ClusterRoleBinding 25 | metadata: 26 | name: prometheus 27 | roleRef: 28 | apiGroup: rbac.authorization.k8s.io 29 | kind: ClusterRole 30 | name: prometheus 31 | subjects: 32 | - kind: ServiceAccount 33 | name: default 34 | namespace: monitoring 35 | {{- end }} -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | orbit-carnival: 4 | build: 5 | context: . 6 | dockerfile: docker/Dockerfile 7 | ports: 8 | - "8001:8001" 9 | - "5005:5005" 10 | environment: 11 | ORBIT_URL: dns:///orbit-server:50056/ 12 | STORE_URL: "http://etcd-store:2379" 13 | entrypoint: sh ./opt/orbitCarnival/entrypoint.sh 14 | volumes: 15 | - ./build/libs/orbit-carnival-release.jar:/opt/orbitCarnival/libs/orbit-carnival.jar 16 | 17 | orbit-server: 18 | image: orbitframework/orbit:2.0.0-alpha.100 19 | ports: 20 | - "5006:5005" 21 | - "50056:50056" 22 | environment: 23 | ORBIT_URL: "orbit-server:50056" 24 | ORBIT_PORT: 50056 25 | 26 | etcd-store: 27 | image: bitnami/etcd:3 28 | ports: 29 | - "2379:2379" 30 | - "2380:2380" 31 | environment: 32 | ALLOW_NONE_AUTHENTICATION: "yes" 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Orbit Carnival Sample 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | jobs: 7 | build: 8 | name: Publish Docker Image 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Sync Repository 13 | uses: actions/checkout@v2 14 | with: 15 | ref: master 16 | - name: Set git credentials 17 | run: | 18 | git config --global user.email "orbit@ea.com" 19 | git config --global user.name "orbit_tools" 20 | - name: Set up JDK 21 | uses: actions/setup-java@v1 22 | with: 23 | java-version: 11 24 | - name: Build Artifacts 25 | run: ./gradlew build fatJar 26 | - name: Build and Publish Docker image to Dockerhub 27 | uses: docker/build-push-action@v1 28 | with: 29 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 30 | password: ${{ secrets.DOCKER_HUB_PASSWORD }} 31 | repository: orbitframework/orbit-sample 32 | dockerfile: ./docker/Dockerfile 33 | tags: latest 34 | -------------------------------------------------------------------------------- /charts/orbit-carnival/templates/prometheus-deployment.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.metrics.enabled }} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: prometheus-deployment 6 | namespace: monitoring 7 | labels: 8 | app: prometheus-server 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: prometheus-server 14 | template: 15 | metadata: 16 | labels: 17 | app: prometheus-server 18 | spec: 19 | containers: 20 | - name: prometheus 21 | image: prom/prometheus 22 | args: 23 | - "--config.file=/etc/prometheus/prometheus.yml" 24 | - "--storage.tsdb.path=/prometheus/" 25 | ports: 26 | - containerPort: 9090 27 | volumeMounts: 28 | - name: prometheus-config-volume 29 | mountPath: /etc/prometheus/ 30 | - name: prometheus-storage-volume 31 | mountPath: /prometheus/ 32 | volumes: 33 | - name: prometheus-config-volume 34 | configMap: 35 | defaultMode: 420 36 | name: prometheus-server-conf 37 | 38 | - name: prometheus-storage-volume 39 | emptyDir: {} 40 | {{- end }} -------------------------------------------------------------------------------- /charts/orbit-carnival/templates/grafana-deployment.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.metrics.enabled }} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: grafana 6 | namespace: monitoring 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: grafana 12 | template: 13 | metadata: 14 | name: grafana 15 | labels: 16 | app: grafana 17 | spec: 18 | containers: 19 | - name: grafana 20 | image: grafana/grafana:latest 21 | ports: 22 | - name: grafana 23 | containerPort: 3000 24 | resources: 25 | limits: 26 | memory: "2Gi" 27 | cpu: "1000m" 28 | requests: 29 | memory: "1Gi" 30 | cpu: "500m" 31 | volumeMounts: 32 | - mountPath: /var/lib/grafana 33 | name: grafana-storage 34 | - mountPath: /etc/grafana/provisioning/datasources 35 | name: grafana-datasources 36 | readOnly: false 37 | volumes: 38 | - name: grafana-storage 39 | emptyDir: {} 40 | - name: grafana-datasources 41 | configMap: 42 | defaultMode: 420 43 | name: grafana-datasources 44 | {{- end }} 45 | -------------------------------------------------------------------------------- /charts/orbit-carnival/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "orbit-carnival.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "orbit-carnival.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "orbit-carnival.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{- define "orbit-carnival.selectorLabels" -}} 35 | app: {{ include "orbit-carnival.name" . }} 36 | release: {{ .Release.Name }} 37 | {{- end -}} 38 | 39 | -------------------------------------------------------------------------------- /src/main/kotlin/orbit/carnival/Carnival.kt: -------------------------------------------------------------------------------- 1 | package orbit.carnival 2 | 3 | import orbit.carnival.actors.* 4 | import orbit.client.OrbitClient 5 | import orbit.client.actor.createProxy 6 | 7 | class Carnival(val orbit: OrbitClient) { 8 | suspend fun playGame(gameId: String, playerId: String, gameTimeMs: Long = 0): PlayedGameResult { 9 | val player = orbit.actorFactory.createProxy(playerId) 10 | 11 | return player.playGame(gameId, gameTimeMs) 12 | } 13 | 14 | suspend fun getPlayer(playerId: String): PlayerResult { 15 | val player = orbit.actorFactory.createProxy(playerId) 16 | val playerDataResult = player.getData() 17 | 18 | return PlayerResult( 19 | playerId = playerId, 20 | rewards = playerDataResult.rewards 21 | ) 22 | } 23 | 24 | suspend fun getGame(gameId: String): GameResult { 25 | val game = orbit.actorFactory.createProxy(gameId) 26 | val gameData = game.loadData() 27 | return GameResult( 28 | gameId = gameId, 29 | name = gameData.name, 30 | timesPlayed = gameData.timesPlayed 31 | ) 32 | } 33 | 34 | fun getGames(): List { 35 | return GameImpl.catalog.games 36 | } 37 | } 38 | 39 | data class GameResult( 40 | val gameId: String, 41 | val name: String, 42 | val timesPlayed: Int 43 | ) 44 | 45 | data class PlayerResult( 46 | val playerId: String, 47 | val rewards: List 48 | ) 49 | -------------------------------------------------------------------------------- /src/main/kotlin/orbit/carnival/actors/repository/etcd/EtcdGameStore.kt: -------------------------------------------------------------------------------- 1 | package orbit.carnival.actors.repository.etcd 2 | 3 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 4 | import com.fasterxml.jackson.module.kotlin.readValue 5 | import io.etcd.jetcd.ByteSequence 6 | import io.etcd.jetcd.Client 7 | import io.etcd.jetcd.options.GetOption 8 | import kotlinx.coroutines.future.await 9 | import orbit.carnival.actors.repository.GameRecord 10 | import orbit.carnival.actors.repository.GameStore 11 | 12 | class EtcdGameStore(url: String) : GameStore { 13 | 14 | val mapper = jacksonObjectMapper() 15 | val keyPrefix = "game" 16 | 17 | private val client = Client.builder().endpoints(url).build().kvClient 18 | 19 | private fun toKey(gameId: String): ByteSequence { 20 | return ByteSequence.from("$keyPrefix/${gameId}".toByteArray()) 21 | } 22 | 23 | override suspend fun get(): List { 24 | val key = ByteSequence.from("\u0000".toByteArray()) 25 | 26 | val option = GetOption.newBuilder() 27 | .withSortField(GetOption.SortTarget.KEY) 28 | .withSortOrder(GetOption.SortOrder.DESCEND) 29 | .withPrefix(ByteSequence.from(keyPrefix.toByteArray())) 30 | .withRange(key) 31 | .build() 32 | 33 | return client.get(key, option).await().kvs.map { game -> 34 | mapper.readValue(game.value.bytes) 35 | } 36 | } 37 | 38 | override suspend fun get(gameId: String): GameRecord? { 39 | val response = client.get(toKey(gameId)).await() 40 | return response.kvs.firstOrNull()?.value?.let { 41 | mapper.readValue(it.bytes) 42 | } 43 | } 44 | 45 | override suspend fun put(game: GameRecord) { 46 | client.put(toKey(game.id), ByteSequence.from(mapper.writeValueAsBytes(game))).await() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/kotlin/orbit/carnival/actors/repository/etcd/EtcdPlayerStore.kt: -------------------------------------------------------------------------------- 1 | package orbit.carnival.actors.repository.etcd 2 | 3 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 4 | import com.fasterxml.jackson.module.kotlin.readValue 5 | import io.etcd.jetcd.ByteSequence 6 | import io.etcd.jetcd.Client 7 | import io.etcd.jetcd.options.GetOption 8 | import kotlinx.coroutines.future.await 9 | import orbit.carnival.actors.repository.PlayerRecord 10 | import orbit.carnival.actors.repository.PlayerStore 11 | 12 | class EtcdPlayerStore(url: String) : PlayerStore { 13 | 14 | val mapper = jacksonObjectMapper() 15 | val keyPrefix = "game" 16 | 17 | private val client = Client.builder().endpoints(url).build().kvClient 18 | 19 | private fun toKey(gameId: String): ByteSequence { 20 | return ByteSequence.from("$keyPrefix/${gameId}".toByteArray()) 21 | } 22 | 23 | override suspend fun get(): List { 24 | val key = ByteSequence.from("\u0000".toByteArray()) 25 | 26 | val option = GetOption.newBuilder() 27 | .withSortField(GetOption.SortTarget.KEY) 28 | .withSortOrder(GetOption.SortOrder.DESCEND) 29 | .withPrefix(ByteSequence.from(keyPrefix.toByteArray())) 30 | .withRange(key) 31 | .build() 32 | 33 | return client.get(key, option).await().kvs.map { game -> 34 | mapper.readValue(game.value.bytes) 35 | } 36 | } 37 | 38 | override suspend fun get(id: String): PlayerRecord? { 39 | val response = client.get(toKey(id)).await() 40 | return response.kvs.firstOrNull()?.value?.let { 41 | mapper.readValue(it.bytes) 42 | } 43 | } 44 | 45 | override suspend fun put(player: PlayerRecord) { 46 | client.put(toKey(player.id), ByteSequence.from(mapper.writeValueAsBytes(player))).await() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/kotlin/orbit/carnival/Server.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 - 2020 Electronic Arts Inc. All rights reserved. 3 | This file is part of the Orbit Project . 4 | See license in LICENSE. 5 | */ 6 | 7 | package orbit.carnival 8 | 9 | import io.ktor.application.* 10 | import io.ktor.http.* 11 | import io.ktor.request.* 12 | import io.ktor.response.* 13 | import io.ktor.routing.* 14 | import orbit.carnival.actors.GameImpl 15 | 16 | class Server( 17 | carnival: Carnival, 18 | application: Application 19 | ) { 20 | init { 21 | application.routing { 22 | get("/") { 23 | call.respondText("Ok", ContentType.Text.Plain) 24 | } 25 | 26 | get("/games") { 27 | call.respond(GameImpl.catalog.games) 28 | } 29 | 30 | get("/game/{gameId}") { 31 | val gameId = call.parameters["gameId"] 32 | if (gameId == null) { 33 | call.respond(HttpStatusCode.BadRequest) 34 | return@get 35 | } 36 | call.respond(carnival.getGame(gameId)) 37 | } 38 | 39 | get("/player/{playerId}") { 40 | val playerId = call.parameters["playerId"] 41 | if (playerId == null) { 42 | call.respond(HttpStatusCode.BadRequest) 43 | return@get 44 | } 45 | call.respond(carnival.getPlayer(playerId)) 46 | } 47 | 48 | post("/player/{playerId}/play") { 49 | val playerId = call.parameters["playerId"] 50 | if (playerId == null) { 51 | call.respond(HttpStatusCode.BadRequest) 52 | return@post 53 | } 54 | val body = call.receive() 55 | val gameId = body.game 56 | println("Player ${playerId} playing game: ${gameId}") 57 | 58 | val result = carnival.playGame(gameId, playerId, body.gameTimeMs) 59 | 60 | call.respond(result) 61 | } 62 | } 63 | } 64 | } 65 | 66 | 67 | data class PlayGameRequest( 68 | val game: String, 69 | val gameTimeMs: Long 70 | ) 71 | -------------------------------------------------------------------------------- /src/main/kotlin/orbit/carnival/actors/Player.kt: -------------------------------------------------------------------------------- 1 | package orbit.carnival.actors 2 | 3 | import com.github.ssedano.hash.JumpConsistentHash 4 | import orbit.carnival.actors.repository.PlayerStore 5 | import orbit.carnival.actors.repository.toRecord 6 | import orbit.client.actor.AbstractActor 7 | import orbit.client.actor.ActorWithStringKey 8 | import orbit.client.actor.createProxy 9 | import orbit.client.addressable.DeactivationReason 10 | import orbit.client.addressable.OnActivate 11 | import orbit.client.addressable.OnDeactivate 12 | import orbit.shared.addressable.Key 13 | 14 | 15 | interface Player : ActorWithStringKey { 16 | suspend fun getData(): PlayerData 17 | suspend fun playGame(gameId: String, gameTimeMs: Long): PlayedGameResult 18 | } 19 | 20 | class PlayerImpl(private val playerStore: PlayerStore) : AbstractActor(), Player { 21 | internal lateinit var rewards: MutableList 22 | 23 | val id: String get() = (this.context.reference.key as Key.StringKey).key 24 | 25 | @OnActivate 26 | suspend fun onActivate() { 27 | loadFromStore() 28 | } 29 | 30 | @OnDeactivate 31 | suspend fun onDeactivate(deactivationReason: DeactivationReason) { 32 | saveToStore() 33 | } 34 | 35 | private suspend fun loadFromStore() { 36 | val loadedPlayer = playerStore.get(id) 37 | 38 | rewards = loadedPlayer?.rewards?.toMutableList() ?: mutableListOf() 39 | } 40 | 41 | private suspend fun saveToStore() { 42 | playerStore.put(this.toRecord()) 43 | } 44 | 45 | override suspend fun getData(): PlayerData { 46 | return PlayerData(rewards = rewards) 47 | } 48 | 49 | override suspend fun playGame(gameId: String, gameTimeMs: Long): PlayedGameResult { 50 | val playerId = id 51 | val jumpConsistentHash = JumpConsistentHash.jumpConsistentHash(playerId, 1000) 52 | val game = context.client.actorFactory.createProxy("$gameId-$jumpConsistentHash") 53 | 54 | val result = game.play(playerId, gameTimeMs) 55 | if (result.winner) { 56 | this@PlayerImpl.rewards.add(result.reward) 57 | } 58 | 59 | saveToStore() 60 | 61 | return result 62 | } 63 | } 64 | 65 | data class PlayerData( 66 | val rewards: List 67 | ) 68 | -------------------------------------------------------------------------------- /src/main/kotlin/orbit/carnival/LoadServer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 - 2020 Electronic Arts Inc. All rights reserved. 3 | This file is part of the Orbit Project . 4 | See license in LICENSE. 5 | */ 6 | 7 | package orbit.carnival 8 | 9 | import io.ktor.application.* 10 | import io.ktor.request.* 11 | import io.ktor.response.* 12 | import io.ktor.routing.* 13 | import java.time.Duration 14 | import java.time.Instant 15 | import kotlin.random.Random 16 | 17 | class LoadServer( 18 | carnival: Carnival, 19 | application: Application 20 | ) { 21 | private var testInProgress = false 22 | 23 | init { 24 | application.routing { 25 | post("/load/play") { 26 | val startTime = Instant.now() 27 | val body = call.receive() 28 | if (!body.concurrent && testInProgress) { 29 | call.respond("Test in progress") 30 | return@post 31 | } 32 | println("Starting load test: ${body.games} games - ${body.players} players - ${body.count} times") 33 | testInProgress = true 34 | val games = carnival.getGames() 35 | val gameCount = Math.min(body.games, games.count()) 36 | 37 | val failures = mutableListOf() 38 | val results = (0..body.count).map { _ -> 39 | val gameId = games[Random.nextInt(0, gameCount)].id 40 | val playerId = "${Random.nextInt(1, body.players + 1)}" 41 | try { 42 | carnival.playGame( 43 | gameId, 44 | playerId, 45 | body.gameTimeMs 46 | ) 47 | } catch (e: Throwable) { 48 | val failure = "Failure Game($gameId): Player $playerId: \n${e.message}" 49 | println(failure) 50 | failures.add(failure) 51 | null 52 | } 53 | }.filterNotNull() 54 | 55 | println("--- Load test complete in ${Duration.between(Instant.now(), startTime).seconds} seconds ---") 56 | println(failures.joinToString("\n")) 57 | testInProgress = false 58 | call.respond(object { 59 | var gamesPlayed = results.count() 60 | var winners = results.count { r -> r.winner } 61 | }) 62 | } 63 | } 64 | } 65 | } 66 | 67 | data class LoadPlayRequest( 68 | val games: Int, 69 | val players: Int, 70 | val count: Int, 71 | val concurrent: Boolean = true, 72 | val gameTimeMs: Long = 0 73 | ) 74 | -------------------------------------------------------------------------------- /Orbit-Carnival.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "7a33402b-9a07-4ef1-8e9e-111160363527", 4 | "name": "Orbit Carnival", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Get All Games", 10 | "request": { 11 | "method": "GET", 12 | "header": [], 13 | "url": { 14 | "raw": "http://localhost:8001/games", 15 | "protocol": "http", 16 | "host": [ 17 | "localhost" 18 | ], 19 | "port": "8001", 20 | "path": [ 21 | "games" 22 | ] 23 | } 24 | }, 25 | "response": [] 26 | }, 27 | { 28 | "name": "Get Game", 29 | "request": { 30 | "method": "GET", 31 | "header": [], 32 | "url": { 33 | "raw": "http://localhost:8001/game/BalloonDarts", 34 | "protocol": "http", 35 | "host": [ 36 | "localhost" 37 | ], 38 | "port": "8001", 39 | "path": [ 40 | "game", 41 | "BalloonDarts" 42 | ] 43 | } 44 | }, 45 | "response": [] 46 | }, 47 | { 48 | "name": "Get Player", 49 | "request": { 50 | "method": "GET", 51 | "header": [], 52 | "url": { 53 | "raw": "http://localhost:8001/player/4", 54 | "protocol": "http", 55 | "host": [ 56 | "localhost" 57 | ], 58 | "port": "8001", 59 | "path": [ 60 | "player", 61 | "4" 62 | ] 63 | } 64 | }, 65 | "response": [] 66 | }, 67 | { 68 | "name": "Play Game", 69 | "request": { 70 | "method": "POST", 71 | "header": [ 72 | { 73 | "key": "content-type", 74 | "value": "application/json" 75 | } 76 | ], 77 | "body": { 78 | "mode": "raw", 79 | "raw": "{\n\t\"game\": \"BalloonDarts\"\n}" 80 | }, 81 | "url": { 82 | "raw": "http://localhost:8001/player/4/play", 83 | "protocol": "http", 84 | "host": [ 85 | "localhost" 86 | ], 87 | "port": "8001", 88 | "path": [ 89 | "player", 90 | "4", 91 | "play" 92 | ] 93 | } 94 | }, 95 | "response": [] 96 | }, 97 | { 98 | "name": "Load Test Games", 99 | "request": { 100 | "method": "POST", 101 | "header": [ 102 | { 103 | "key": "content-type", 104 | "value": "application/json" 105 | } 106 | ], 107 | "body": { 108 | "mode": "raw", 109 | "raw": "{\n \"games\": 5,\n \"players\": 4,\n \"count\": 80\n}" 110 | }, 111 | "url": { 112 | "raw": "http://localhost:8001/load/play", 113 | "protocol": "http", 114 | "host": [ 115 | "localhost" 116 | ], 117 | "port": "8001", 118 | "path": [ 119 | "load", 120 | "play" 121 | ] 122 | } 123 | }, 124 | "response": [] 125 | } 126 | ], 127 | "protocolProfileBehavior": {} 128 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /src/main/kotlin/orbit/carnival/App.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 - 2019 Electronic Arts Inc. All rights reserved. 3 | This file is part of the Orbit Project . 4 | See license in LICENSE. 5 | */ 6 | 7 | package orbit.carnival 8 | 9 | import com.fasterxml.jackson.databind.SerializationFeature 10 | import io.ktor.application.* 11 | import io.ktor.features.* 12 | import io.ktor.http.* 13 | import io.ktor.jackson.* 14 | import io.ktor.response.* 15 | import io.ktor.server.engine.* 16 | import io.ktor.server.netty.* 17 | import kotlinx.coroutines.runBlocking 18 | import orbit.carnival.actors.GameImpl 19 | import orbit.carnival.actors.PlayerImpl 20 | import orbit.carnival.actors.repository.GameStore 21 | import orbit.carnival.actors.repository.PlayerStore 22 | import orbit.carnival.actors.repository.etcd.EtcdGameStore 23 | import orbit.carnival.actors.repository.etcd.EtcdPlayerStore 24 | import orbit.carnival.actors.repository.local.LocalGameStore 25 | import orbit.carnival.actors.repository.local.LocalPlayerStore 26 | import orbit.client.OrbitClient 27 | import orbit.client.OrbitClientConfig 28 | import orbit.client.addressable.Addressable 29 | import orbit.client.addressable.AddressableConstructor 30 | import orbit.util.di.ExternallyConfigured 31 | import org.kodein.di.Instance 32 | import org.kodein.di.Kodein 33 | import org.kodein.di.TT 34 | import org.kodein.di.generic.bind 35 | import org.kodein.di.generic.instance 36 | import org.kodein.di.generic.provider 37 | import org.kodein.di.generic.singleton 38 | import java.text.DateFormat 39 | import java.time.Duration 40 | 41 | fun main() { 42 | runBlocking { 43 | val storeUrl = System.getenv("STORE_URL") 44 | 45 | val kodein = Kodein { 46 | bind() with singleton { if (storeUrl != null) EtcdGameStore(storeUrl) else LocalGameStore() } 47 | bind() with singleton { if (storeUrl != null) EtcdPlayerStore(storeUrl) else LocalPlayerStore() } 48 | bind() with provider { PlayerImpl(instance()) } 49 | bind() with provider { GameImpl(instance()) } 50 | } 51 | 52 | val orbitUrl = System.getenv("ORBIT_URL") ?: "dns:///localhost:50056/" 53 | val orbitClient = OrbitClient( 54 | OrbitClientConfig( 55 | namespace = "carnival", 56 | packages = listOf("orbit.carnival.actors"), 57 | grpcEndpoint = orbitUrl, 58 | addressableTTL = Duration.ofSeconds(6000), 59 | addressableConstructor = KodeinAddressableConstructor.KodeinAddressableConstructorSingleton, 60 | containerOverrides = { 61 | instance(kodein) 62 | }, 63 | railCount = 512 64 | ) 65 | ) 66 | 67 | println("Connecting to Orbit at $orbitUrl") 68 | 69 | orbitClient.start() 70 | 71 | val carnival = Carnival(orbitClient) 72 | 73 | embeddedServer(Netty, 8001) { 74 | install(DefaultHeaders) 75 | 76 | install(ContentNegotiation) { 77 | jackson { 78 | enable(SerializationFeature.INDENT_OUTPUT) 79 | dateFormat = DateFormat.getDateInstance() 80 | deactivateDefaultTyping() 81 | } 82 | } 83 | 84 | install(StatusPages) { 85 | exception { cause -> 86 | println("Exception: ${cause}") 87 | call.respond(HttpStatusCode.InternalServerError) 88 | } 89 | } 90 | 91 | Server(carnival, this) 92 | LoadServer(carnival, this) 93 | 94 | }.start() 95 | 96 | Runtime.getRuntime().addShutdownHook(object : Thread() { 97 | override fun run() = runBlocking { 98 | println("Gracefully shutting down") 99 | orbitClient.stop() 100 | println("Shutdown complete") 101 | } 102 | }) 103 | 104 | println("The Carnival has started") 105 | } 106 | } 107 | 108 | class KodeinAddressableConstructor(private val kodein: Kodein) : AddressableConstructor { 109 | object KodeinAddressableConstructorSingleton : ExternallyConfigured { 110 | override val instanceType = KodeinAddressableConstructor::class.java 111 | } 112 | 113 | override fun constructAddressable(clazz: Class): Addressable { 114 | val addressable: Addressable by kodein.Instance(TT(clazz)) 115 | 116 | return addressable 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/kotlin/orbit/carnival/actors/Game.kt: -------------------------------------------------------------------------------- 1 | package orbit.carnival.actors 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory 5 | import com.fasterxml.jackson.module.kotlin.readValue 6 | import kotlinx.coroutines.delay 7 | import orbit.carnival.actors.repository.GameStore 8 | import orbit.carnival.actors.repository.toRecord 9 | import orbit.client.actor.AbstractActor 10 | import orbit.client.actor.ActorWithStringKey 11 | import orbit.client.addressable.DeactivationReason 12 | import orbit.client.addressable.OnActivate 13 | import orbit.client.addressable.OnDeactivate 14 | import orbit.shared.addressable.Key 15 | import kotlin.random.Random 16 | 17 | interface Game : ActorWithStringKey { 18 | suspend fun play(playerId: String, gameTimeMs: Long): PlayedGameResult 19 | suspend fun loadData(): GameData 20 | } 21 | 22 | class GameImpl(private val gameStore: GameStore) : AbstractActor(), Game { 23 | val id: String get() = (this.context.reference.key as Key.StringKey).key 24 | val gameId: String get() = id.split("-")[0] 25 | private lateinit var gameData: Catalog.Game 26 | 27 | private val baseWinningOdds = .5 28 | 29 | internal var results = mutableListOf() 30 | 31 | @OnActivate 32 | suspend fun onActivate() { 33 | loadFromStore() 34 | } 35 | 36 | @OnDeactivate 37 | suspend fun onDeactivate(deactivationReason: DeactivationReason) { 38 | saveToStore() 39 | } 40 | 41 | suspend fun loadFromStore() { 42 | gameData = catalog.games.firstOrNull() { game -> 43 | game.id == gameId 44 | } ?: throw IllegalArgumentException("Game does not exist") 45 | 46 | val savedGame = gameStore.get(id) 47 | 48 | this.results = savedGame?.results?.toMutableList() ?: mutableListOf() 49 | } 50 | 51 | suspend fun saveToStore() { 52 | gameStore.put(this.toRecord()) 53 | } 54 | 55 | companion object { 56 | @JvmStatic 57 | var catalog: Catalog 58 | 59 | init { 60 | val catalogContent = this::class.java.getResource("/games.yml").readText() 61 | catalog = ObjectMapper(YAMLFactory()).readValue(catalogContent) 62 | } 63 | } 64 | 65 | override suspend fun play(playerId: String, gameTimeMs: Long): PlayedGameResult { 66 | delay(gameTimeMs) 67 | 68 | val previousResult = results.lastOrNull() 69 | val replay = previousResult?.playerId == playerId && previousResult.level < 4 70 | var level = if (replay) previousResult!!.level else 0 71 | 72 | val win = Random.nextDouble() < (baseWinningOdds / (level + 1)) 73 | if (win) level++ 74 | 75 | val prize = if (win) 76 | (when (level) { 77 | 1 -> gameData.prizes.small 78 | 2 -> gameData.prizes.medium 79 | 3 -> gameData.prizes.large 80 | 4 -> gameData.prizes.grand 81 | else -> listOf() 82 | }).random() else "" 83 | 84 | val result = PlayedGameResult( 85 | name = gameData.name, 86 | playerId = playerId, 87 | winner = win, 88 | reward = prize, 89 | prizeLevel = if (win) (when (level) { 90 | 1 -> "small" 91 | 2 -> "medium" 92 | 3 -> "large" 93 | 4 -> "grand" 94 | else -> "" 95 | }) else "", 96 | level = level 97 | ) 98 | 99 | results.add(result) 100 | 101 | saveToStore() 102 | return result 103 | } 104 | 105 | 106 | override suspend fun loadData(): GameData { 107 | return GameData( 108 | name = gameData.name, 109 | timesPlayed = results.count() 110 | ) 111 | } 112 | } 113 | 114 | data class Catalog(val games: List = listOf()) { 115 | data class Game( 116 | val id: String = "", 117 | val name: String = "", 118 | val theme: String = "", 119 | val prizes: PrizeList = PrizeList() 120 | ) 121 | 122 | data class PrizeList( 123 | val small: List = listOf(), 124 | val medium: List = listOf(), 125 | val large: List = listOf(), 126 | val grand: List = listOf() 127 | ) 128 | } 129 | 130 | data class GameData( 131 | val name: String, 132 | val timesPlayed: Int 133 | ) 134 | 135 | data class PlayedGameResult( 136 | val name: String, 137 | val playerId: String, 138 | val winner: Boolean, 139 | val level: Int, 140 | val prizeLevel: String, 141 | val reward: String 142 | ) 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Orbit Sample App (The Carnival) 2 | 3 | This is a sample app to help illustrate the concepts of an Orbit Client application. It simulates a carnival games, players, and prizes! 4 | 5 | ## Getting Started 6 | 7 | This sample app will install Orbit Server, its depenedencies, and a client application called Carnival. 8 | 9 | ### Prerequisites 10 | * [Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube) or [Docker for Desktop](https://docs.docker.com/get-docker/) 11 | * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) Kubernetes Command Line Tools 12 | * [Skaffold](https://skaffold.dev) to easily bring up the environment 13 | * [Insomnia](https://insomnia.rest/) or [Postman](https://www.postman.com/) for testing 14 | 15 | ### Set up Kubernetes environment 16 | 17 | #### Minikube 18 | 19 | After [installing](https://kubernetes.io/docs/tasks/tools/install-minikube) Minikube, start up a new Minikube instance. 20 | ```shell script 21 | > minikube start 22 | ``` 23 | 24 | To verify your Minikube VM is working, you can use the minikube status command. 25 | ```shell script 26 | > minikube status 27 | m01 28 | host: Running 29 | kubelet: Running 30 | apiserver: Running 31 | kubeconfig: Configured 32 | ``` 33 | 34 | #### Docker for Desktop 35 | With Docker for Desktop running, open settings and enable Kubernetes. 36 | 37 | * [Mac](https://docs.docker.com/docker-for-mac/kubernetes/) 38 | * [Windows](https://docs.docker.com/docker-for-windows/kubernetes/) 39 | 40 | ### Kubectl Context and Namespace 41 | We want to assure all the further commands are happening in the right Kubernetes cluster and isolate the project using a namespace. Tp use Docker For Desktop, replace `minikube` with `docker-desktop`. 42 | 43 | ```shell script 44 | > kubectl config use-context minikube 45 | Switched to context "minikube". 46 | > kubectl create namespace orbit-carnival 47 | namespace/orbit-carnival created 48 | > kubectl config set-context --current --namespace=orbit-carnival 49 | Context "minikube" modified. 50 | ``` 51 | 52 | Verify the orbit-carnival namespace is selected in the right context. 53 | ```shell script 54 | > kubectl config get-contexts 55 | CURRENT NAME CLUSTER AUTHINFO NAMESPACE 56 | docker-desktop docker-desktop docker-desktop 57 | docker-for-desktop docker-desktop docker-desktop 58 | * minikube minikube minikube orbit-carnival 59 | ``` 60 | 61 | ### Clone the repository 62 | 63 | ```shell script 64 | > git clone git@github.com:orbit/orbit-sample.git 65 | ``` 66 | 67 | ### Run Skaffold 68 | 69 | ```shell script 70 | > skaffold dev --port-forward 71 | ``` 72 | 73 | To assure the proper pods are running, you can run kubectl get pod. You should see the pods for the Carnival, Orbit Server, and the Node and Addressable directories. 74 | 75 | ```shell script 76 | > kubectl get pod 77 | NAME READY STATUS RESTARTS AGE 78 | orbit-addressable-directory-8666f4fbc6-j6lmz 1/1 Running 0 59s 79 | orbit-carnival-5c6f59bb-kw2zr 1/1 Running 0 59s 80 | orbit-node-directory-bdb45ff8d-g9fjt 1/1 Running 0 59s 81 | orbit-server-78fb97dd58-lx466 1/1 Running 0 59s 82 | ``` 83 | 84 | Fun tip: Use the [`watch`](https://www.geeksforgeeks.org/watch-command-in-linux-with-examples/ 85 | ) command to keep a live view of running containers. 86 | ```shell script 87 | > watch kubectl get pod 88 | ``` 89 | 90 | ### Test 91 | 92 | The Carnival test app exposes a REST endpoint for playing the game. By default, the carnival is exposed at `http://localhost:8001` 93 | 94 | | Method | Url | Payload 95 | |--------|-------------------------|----------- 96 | | GET | /games | 97 | | GET | /game/{gameId} | 98 | | GET | /player/{playerId} | 99 | | POST | /player/{playerId}/play | ex. { "game": "BalloonDarts" } 100 | 101 | 102 | One more endpoint exists for load testing by continuously playing games: 103 | 104 | | Method | Url | Payload | 105 | |---|---|--- 106 | | POST | /load/play | ex. { "games": 5, "players": 4, "count": 800 } 107 | 108 | To help more easily test the endpoints, you can drive it through a REST request application like Insomnia or Postman. These are some collections to get you started. 109 | * Insomnia [collection](https://github.com/orbit/orbit-sample/blob/master/Orbit-Carnival.insomnia_collection.json) 110 | * Postman [collection](https://github.com/orbit/orbit-sample/blob/master/Orbit-Carnival.postman_collection.json) 111 | 112 | 113 | -------------------------------------------------------------------------------- /charts/orbit-carnival/templates/prometheus-config.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.metrics.enabled }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: prometheus-server-conf 6 | labels: 7 | name: prometheus-server-conf 8 | namespace: monitoring 9 | data: 10 | prometheus.rules: |- 11 | groups: 12 | - name: devopscube demo alert 13 | rules: 14 | - alert: High Pod Memory 15 | expr: sum(container_memory_usage_bytes) > 1 16 | for: 1m 17 | labels: 18 | severity: slack 19 | annotations: 20 | summary: High Memory Usage 21 | prometheus.yml: |- 22 | global: 23 | scrape_interval: 5s 24 | evaluation_interval: 5s 25 | rule_files: 26 | - /etc/prometheus/prometheus.rules 27 | alerting: 28 | alertmanagers: 29 | - scheme: http 30 | static_configs: 31 | - targets: 32 | - "alertmanager.monitoring.svc:9093" 33 | 34 | scrape_configs: 35 | - job_name: 'kubernetes-apiservers' 36 | 37 | kubernetes_sd_configs: 38 | - role: endpoints 39 | scheme: https 40 | 41 | tls_config: 42 | ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt 43 | bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token 44 | 45 | relabel_configs: 46 | - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name] 47 | action: keep 48 | regex: default;kubernetes;https 49 | 50 | - job_name: 'kubernetes-nodes' 51 | 52 | scheme: https 53 | 54 | tls_config: 55 | ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt 56 | bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token 57 | 58 | kubernetes_sd_configs: 59 | - role: node 60 | 61 | relabel_configs: 62 | - action: labelmap 63 | regex: __meta_kubernetes_node_label_(.+) 64 | - target_label: __address__ 65 | replacement: kubernetes.default.svc:443 66 | - source_labels: [__meta_kubernetes_node_name] 67 | regex: (.+) 68 | target_label: __metrics_path__ 69 | replacement: /api/v1/nodes/${1}/proxy/metrics 70 | 71 | 72 | - job_name: 'kubernetes-pods' 73 | 74 | kubernetes_sd_configs: 75 | - role: pod 76 | 77 | relabel_configs: 78 | - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] 79 | action: keep 80 | regex: true 81 | - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] 82 | action: replace 83 | target_label: __metrics_path__ 84 | regex: (.+) 85 | - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] 86 | action: replace 87 | regex: ([^:]+)(?::\d+)?;(\d+) 88 | replacement: $1:$2 89 | target_label: __address__ 90 | - action: labelmap 91 | regex: __meta_kubernetes_pod_label_(.+) 92 | - source_labels: [__meta_kubernetes_namespace] 93 | action: replace 94 | target_label: kubernetes_namespace 95 | - source_labels: [__meta_kubernetes_pod_name] 96 | action: replace 97 | target_label: kubernetes_pod_name 98 | 99 | - job_name: 'kube-state-metrics' 100 | static_configs: 101 | - targets: ['kube-state-metrics.kube-system.svc.cluster.local:8080'] 102 | 103 | - job_name: 'kubernetes-cadvisor' 104 | 105 | scheme: https 106 | 107 | tls_config: 108 | ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt 109 | bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token 110 | 111 | kubernetes_sd_configs: 112 | - role: node 113 | 114 | relabel_configs: 115 | - action: labelmap 116 | regex: __meta_kubernetes_node_label_(.+) 117 | - target_label: __address__ 118 | replacement: kubernetes.default.svc:443 119 | - source_labels: [__meta_kubernetes_node_name] 120 | regex: (.+) 121 | target_label: __metrics_path__ 122 | replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor 123 | 124 | - job_name: 'kubernetes-service-endpoints' 125 | 126 | kubernetes_sd_configs: 127 | - role: endpoints 128 | 129 | relabel_configs: 130 | - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape] 131 | action: keep 132 | regex: true 133 | - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme] 134 | action: replace 135 | target_label: __scheme__ 136 | regex: (https?) 137 | - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path] 138 | action: replace 139 | target_label: __metrics_path__ 140 | regex: (.+) 141 | - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port] 142 | action: replace 143 | target_label: __address__ 144 | regex: ([^:]+)(?::\d+)?;(\d+) 145 | replacement: $1:$2 146 | - action: labelmap 147 | regex: __meta_kubernetes_service_label_(.+) 148 | - source_labels: [__meta_kubernetes_namespace] 149 | action: replace 150 | target_label: kubernetes_namespace 151 | - source_labels: [__meta_kubernetes_service_name] 152 | action: replace 153 | target_label: kubernetes_name 154 | {{- end }} 155 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /Orbit-Carnival.insomnia_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "_type": "export", 3 | "__export_format": 4, 4 | "__export_date": "2020-05-22T19:00:41.319Z", 5 | "__export_source": "insomnia.desktop.app:v7.1.1", 6 | "resources": [ 7 | { 8 | "_id": "req_6004a2b548794b2aaa5aec8bd993d4b0", 9 | "authentication": {}, 10 | "body": {}, 11 | "created": 1590173986823, 12 | "description": "", 13 | "headers": [], 14 | "isPrivate": false, 15 | "metaSortKey": -1586901503843, 16 | "method": "GET", 17 | "modified": 1590173997359, 18 | "name": "Games", 19 | "parameters": [], 20 | "parentId": "fld_8f70d3de33fe4fbbbc38aae510792e9a", 21 | "settingDisableRenderRequestBody": false, 22 | "settingEncodeUrl": true, 23 | "settingFollowRedirects": "global", 24 | "settingRebuildPath": true, 25 | "settingSendCookies": true, 26 | "settingStoreCookies": true, 27 | "url": "http://localhost:8001/games", 28 | "_type": "request" 29 | }, 30 | { 31 | "_id": "fld_8f70d3de33fe4fbbbc38aae510792e9a", 32 | "created": 1590173986805, 33 | "description": "", 34 | "environment": {}, 35 | "environmentPropertyOrder": null, 36 | "metaSortKey": -1580333352388, 37 | "modified": 1590173986805, 38 | "name": "Orbit-Carnival", 39 | "parentId": "wrk_143f6e934c7b487588ff5b692b761ad5", 40 | "_type": "request_group" 41 | }, 42 | { 43 | "_id": "wrk_143f6e934c7b487588ff5b692b761ad5", 44 | "created": 1573079854806, 45 | "description": "", 46 | "modified": 1573079854806, 47 | "name": "Insomnia", 48 | "parentId": null, 49 | "_type": "workspace" 50 | }, 51 | { 52 | "_id": "req_d797502b14b74e1facb3d6e8f8a1dc0b", 53 | "authentication": {}, 54 | "body": {}, 55 | "created": 1590173986831, 56 | "description": "", 57 | "headers": [], 58 | "isPrivate": false, 59 | "metaSortKey": -1582796410975.1875, 60 | "method": "GET", 61 | "modified": 1590174003949, 62 | "name": "Game", 63 | "parameters": [], 64 | "parentId": "fld_8f70d3de33fe4fbbbc38aae510792e9a", 65 | "settingDisableRenderRequestBody": false, 66 | "settingEncodeUrl": true, 67 | "settingFollowRedirects": "global", 68 | "settingRebuildPath": true, 69 | "settingSendCookies": true, 70 | "settingStoreCookies": true, 71 | "url": "http://localhost:8001/game/BalloonDarts", 72 | "_type": "request" 73 | }, 74 | { 75 | "_id": "req_3b56d845cdc741879c8a349854a60399", 76 | "authentication": {}, 77 | "body": {}, 78 | "created": 1590173986830, 79 | "description": "", 80 | "headers": [], 81 | "isPrivate": false, 82 | "metaSortKey": -1578691318107.375, 83 | "method": "GET", 84 | "modified": 1590174008067, 85 | "name": "Player", 86 | "parameters": [], 87 | "parentId": "fld_8f70d3de33fe4fbbbc38aae510792e9a", 88 | "settingDisableRenderRequestBody": false, 89 | "settingEncodeUrl": true, 90 | "settingFollowRedirects": "global", 91 | "settingRebuildPath": true, 92 | "settingSendCookies": true, 93 | "settingStoreCookies": true, 94 | "url": "http://localhost:8001/player/4", 95 | "_type": "request" 96 | }, 97 | { 98 | "_id": "req_da3740d4cf564e4aa9973d7303162fc8", 99 | "authentication": {}, 100 | "body": { 101 | "mimeType": "application/json", 102 | "text": "{\n\t\"game\": \"BalloonDarts\"\n}" 103 | }, 104 | "created": 1590173986828, 105 | "description": "", 106 | "headers": [ 107 | { 108 | "id": "pair_b61a370bf11542b294f802f6d1c1678c", 109 | "name": "Content-Type", 110 | "value": "application/json" 111 | } 112 | ], 113 | "isPrivate": false, 114 | "metaSortKey": -1577049280960.25, 115 | "method": "POST", 116 | "modified": 1590174011323, 117 | "name": "Play Game", 118 | "parameters": [], 119 | "parentId": "fld_8f70d3de33fe4fbbbc38aae510792e9a", 120 | "settingDisableRenderRequestBody": false, 121 | "settingEncodeUrl": true, 122 | "settingFollowRedirects": "global", 123 | "settingRebuildPath": true, 124 | "settingSendCookies": true, 125 | "settingStoreCookies": true, 126 | "url": "http://localhost:8001/player/4/play", 127 | "_type": "request" 128 | }, 129 | { 130 | "_id": "req_4cb5f65c78a44e88808a5ea08259cedf", 131 | "authentication": {}, 132 | "body": { 133 | "mimeType": "application/json", 134 | "text": "{\n \"games\": 5,\n \"players\": 4,\n \"count\": 80\n}" 135 | }, 136 | "created": 1590173986833, 137 | "description": "", 138 | "headers": [ 139 | { 140 | "id": "pair_b61a370bf11542b294f802f6d1c1678c", 141 | "name": "Content-Type", 142 | "value": "application/json" 143 | } 144 | ], 145 | "isPrivate": false, 146 | "metaSortKey": -1575407243813.125, 147 | "method": "POST", 148 | "modified": 1590174014413, 149 | "name": "Load Test Games", 150 | "parameters": [], 151 | "parentId": "fld_8f70d3de33fe4fbbbc38aae510792e9a", 152 | "settingDisableRenderRequestBody": false, 153 | "settingEncodeUrl": true, 154 | "settingFollowRedirects": "global", 155 | "settingRebuildPath": true, 156 | "settingSendCookies": true, 157 | "settingStoreCookies": true, 158 | "url": "http://localhost:8001/load/play", 159 | "_type": "request" 160 | }, 161 | { 162 | "_id": "env_e5f8845b87dcf29aff49e6a5c286ac27f9831e82", 163 | "color": null, 164 | "created": 1573079854841, 165 | "data": { 166 | "host": "http://localhost:8001" 167 | }, 168 | "dataPropertyOrder": { 169 | "&": [ 170 | "host" 171 | ] 172 | }, 173 | "isPrivate": false, 174 | "metaSortKey": 1573079854841, 175 | "modified": 1586901545173, 176 | "name": "Base Environment", 177 | "parentId": "wrk_143f6e934c7b487588ff5b692b761ad5", 178 | "_type": "environment" 179 | }, 180 | { 181 | "_id": "jar_e5f8845b87dcf29aff49e6a5c286ac27f9831e82", 182 | "cookies": [], 183 | "created": 1573079854843, 184 | "modified": 1573079854843, 185 | "name": "Default Jar", 186 | "parentId": "wrk_143f6e934c7b487588ff5b692b761ad5", 187 | "_type": "cookie_jar" 188 | }, 189 | { 190 | "_id": "env_1ed214e2ca2e4f5495cceffe3a06b331", 191 | "color": null, 192 | "created": 1586901559048, 193 | "data": { 194 | "host": "http://localhost:8001", 195 | "serverHost": "dns:///localhost:50056" 196 | }, 197 | "dataPropertyOrder": { 198 | "&": [ 199 | "host", 200 | "serverHost" 201 | ] 202 | }, 203 | "isPrivate": false, 204 | "metaSortKey": 1586901559048, 205 | "modified": 1590165281510, 206 | "name": "Local", 207 | "parentId": "env_e5f8845b87dcf29aff49e6a5c286ac27f9831e82", 208 | "_type": "environment" 209 | }, 210 | { 211 | "_id": "env_f131c8c8f0d04978bf1ba9990acc4b4f", 212 | "color": null, 213 | "created": 1587493071815, 214 | "data": { 215 | "host": "http://192.168.64.30" 216 | }, 217 | "dataPropertyOrder": { 218 | "&": [ 219 | "host" 220 | ] 221 | }, 222 | "isPrivate": false, 223 | "metaSortKey": 1586901559098, 224 | "modified": 1587595245902, 225 | "name": "Kube", 226 | "parentId": "env_e5f8845b87dcf29aff49e6a5c286ac27f9831e82", 227 | "_type": "environment" 228 | } 229 | ] 230 | } -------------------------------------------------------------------------------- /src/main/resources/games.yml: -------------------------------------------------------------------------------- 1 | # Games and prizes care of Carnival Games for Nintendo Wii 2 | # https://en.wikipedia.org/wiki/Carnival_Games 3 | # https://gamefaqs.gamespot.com/wii/939414-carnival-games/faqs/50177 4 | games: 5 | - id: AlleyBall 6 | name: Alley Ball 7 | theme: Dinosaurs 8 | prizes: 9 | small: 10 | - Jurassic Plant 11 | - Saber Claw 12 | - Talisman 13 | - Dino-Print 14 | medium: 15 | - Flyer-O-Saurus 16 | - T-Rex Necklace 17 | - Mr. Pinchy 18 | large: 19 | - Brontosaurus 20 | - Stegosaurus 21 | grand: 22 | - T-Rex 23 | - id: TestYourStrength 24 | name: Test Your Strength 25 | theme: Ninjas 26 | prizes: 27 | small: 28 | - Throwing Star 29 | - Fighting Fan 30 | - Butterfly Sword 31 | - Jade Dagger 32 | medium: 33 | - Pink Handled Sai 34 | - Plastic Nunchuck 35 | - Ninja Necklace 36 | large: 37 | - Crimson Ninja 38 | - Black Ninja 39 | grand: 40 | - Shaolin 41 | - id: Hoops 42 | name: Hoops 43 | theme: Police/Fire Depts. 44 | prizes: 45 | small: 46 | - Whistle 47 | - Fireman's Hat 48 | - Toy Handcuffs 49 | - Nurse Cap 50 | medium: 51 | - Stuffed officer 52 | - Club Light 53 | - Fire Extinguisher 54 | large: 55 | - Engine No. 9 56 | - Wee-ew Wee-ew 57 | grand: 58 | - Rescue Copter 59 | - id: CollectionPlate 60 | name: Collection Plate 61 | theme: Party Favors 62 | prizes: 63 | small: 64 | - Jacks 65 | - Paddle Ball 66 | - Party Mask 67 | - Deck-O-Cards 68 | medium: 69 | - Red Luft Balloon 70 | - Bubbles 71 | - Beach Ball 72 | large: 73 | - Mr. Bullhorn 74 | - Zipper-Dee-Do 75 | grand: 76 | - Lil Squirt 77 | - id: ClownSplash 78 | name: Clown Splash 79 | theme: Ocean Creatures 80 | prizes: 81 | small: 82 | - Sea Horse 83 | - Shell of the Ocean 84 | - D-Lish the Lobster 85 | - Plush Fish 86 | medium: 87 | - Gingis Tortus 88 | - Happy Starfish 89 | - Six Legged Octopus 90 | large: 91 | - Lu Lu 92 | - Swimmy 93 | grand: 94 | - Sharkster 95 | - id: HoleinOne 96 | name: Hole in One 97 | theme: Fancy items 98 | prizes: 99 | small: 100 | - Silver Star Earrings 101 | - Silver Plated Watch 102 | - Heart Pendant 103 | - 20CT Bling Ring 104 | medium: 105 | - Cool Dudes 106 | - Walking Stick 107 | - Silverlike Money Clip 108 | large: 109 | - 9 Sided Dice 110 | - Cat Daddy Ride 111 | grand: 112 | - Goblet-O-Bling 113 | - id: RingToss 114 | name: Ring Toss 115 | theme: Medieval 116 | prizes: 117 | small: 118 | - Magical Unicorn 119 | - Battle Helm 120 | - King David's Banner 121 | - Merlin's Wand 122 | medium: 123 | - Stuffed Monk 124 | - Easton's Castle 125 | - Toy Bow 126 | large: 127 | - Puffy 128 | - Dragon Slayer 129 | grand: 130 | - Good Knight 131 | - id: KaPow 132 | name: Ka-pow 133 | theme: Vehicles 134 | prizes: 135 | small: 136 | - Toy Plane 137 | - Electro-Spin 138 | - Cucumber Car 139 | - Mini Racer 140 | medium: 141 | - Paper Boat 142 | - Toy Hybrid 143 | - Ol' Jalopy 144 | large: 145 | - Choo Choo 146 | - The Baron 147 | grand: 148 | - Mr. Tubbs 149 | - id: SpilledMilk 150 | name: Spilled Milk 151 | theme: Pirate items 152 | prizes: 153 | small: 154 | - Pirate Pin 155 | - Cannon 156 | - Skull Neckpiece 157 | - Golden Dubloons 158 | medium: 159 | - Pirate Chest 160 | - Jolly Roger 161 | - Anchors Away 162 | large: 163 | - Polly 164 | - Pirate Ship 165 | grand: 166 | - Skully One-Eye 167 | - id: FrogLeap 168 | name: Frog Leap 169 | theme: Construction 170 | prizes: 171 | small: 172 | - Moe Scooper 173 | - Tiny Dumper 174 | - Mixer Truck 175 | - Front Loader 176 | medium: 177 | - Old Yeller 178 | - Ditch Digger 179 | - Combine 180 | large: 181 | - Wrex 182 | - Big Dumper 183 | grand: 184 | - Forklift 185 | - id: BalloonDarts 186 | name: Balloon Darts 187 | theme: Clowns 188 | prizes: 189 | small: 190 | - Painted Face 191 | - Honk'N Horn 192 | - Disguise 193 | - Clown's Sock 194 | medium: 195 | - Clown Doll 196 | - Squirting Flower 197 | - Goofball Award 198 | large: 199 | - Pop-N-Shout 200 | - Dizzy Larry 201 | grand: 202 | - Roll'N On Dub 203 | - id: BowlerCoaster 204 | name: Bowler Coaster 205 | theme: Tropical Animals 206 | prizes: 207 | small: 208 | - Now-N-Later Gator 209 | - Bunch-O-Bananas 210 | - Scary Totem 211 | - Binoculars 212 | medium: 213 | - Wild Mule 214 | - Elephant 215 | - Squacker 216 | large: 217 | - King of the Jungle 218 | - Drive-U-Bananas 219 | grand: 220 | - Goldie 221 | - id: NervesOSteel 222 | name: Nerves-O-Steel 223 | theme: Farm animals 224 | prizes: 225 | small: 226 | - Piggy Bank 227 | - Bo-Sheep 228 | - Good Look'N Chick 229 | - Tom 230 | medium: 231 | - Fuzzy Bunny 232 | - Mallard 233 | - Whinny 234 | large: 235 | - Old Betsy 236 | - Rooster 237 | grand: 238 | - Sit Rover Sit 239 | - id: LuckyCups 240 | name: Lucky Cups 241 | theme: Robots 242 | prizes: 243 | small: 244 | - The Seafth 1000 245 | - Ball Bot 246 | - Square Bot 247 | - Rat Bot 248 | medium: 249 | - Pogo Bot 250 | - The Seafth 2000 251 | - L08 L337 252 | large: 253 | - Row-Bot 254 | - Domo Arigato 255 | grand: 256 | - Colozzle 257 | - id: DayAtTheRaces 258 | name: Day at the Races 259 | theme: Instruments 260 | prizes: 261 | small: 262 | - Pan's Flute 263 | - Violin 264 | - J12 Tune Maker 265 | - Hippy Drum 266 | medium: 267 | - Mrs. Microphone 268 | - Horn of Plenty 269 | - Maracas' 270 | large: 271 | - Captain Piano 272 | - Drum 273 | grand: 274 | - Blues Blower 275 | - id: DunkTank 276 | name: Dunk Tank 277 | theme: Outer Space 278 | prizes: 279 | small: 280 | - Space Breather 281 | - Odd Martian 282 | - Mini-Zap 283 | - Go Shuttle 284 | medium: 285 | - Grayman Syndrome 286 | - Octo Looker 287 | - Old Greenie 288 | large: 289 | - Spaceship Trouble 290 | - Disco Alien 291 | grand: 292 | - Moon Ship 293 | - id: ShootingGallery 294 | name: Shooting Gallery 295 | theme: Animals 296 | prizes: 297 | small: 298 | - Giant Panda 299 | - Marmet 300 | - Tiger Cub 301 | - Snake Thang 302 | medium: 303 | - Sancho 304 | - K-Walla 305 | - Emperor P 306 | large: 307 | - Rapp'N G-Rafe 308 | - Flying Bear 309 | grand: 310 | - Berry 311 | - id: PigskinPass 312 | name: Pigskin Pass 313 | theme: Military 314 | prizes: 315 | small: 316 | - Dud Skud 317 | - Armyman 318 | - T1 319 | - Carrier 320 | medium: 321 | - War Chopper 322 | - Plastic Grenade 323 | - Atv 324 | large: 325 | - Jet 326 | - Tank 327 | grand: 328 | - SS Rosemary 329 | - id: ShootForTheStars 330 | name: Shoot for the Stars 331 | theme: Sports 332 | prizes: 333 | small: 334 | - Football 335 | - Basketball 336 | - Ping Pong Set 337 | - Mini-Bowling 338 | medium: 339 | - Baseball 340 | - Red Golden Gloves 341 | - Hockey 342 | large: 343 | - Skateboard 344 | - Flying Disk 345 | grand: 346 | - Nice Comeback 347 | - id: BucketsOfFun 348 | name: Buckets of Fun 349 | theme: Monsters 350 | prizes: 351 | small: 352 | - Monster Purse 353 | - Luxnor 354 | - Joseama 355 | - Occular 356 | medium: 357 | - Diabloie 358 | - Rabbidus 359 | - Dragon Spawn 360 | large: 361 | - Ugly Buggly 362 | - Squeen Eye 363 | grand: 364 | - Frogenstein -------------------------------------------------------------------------------- /grafana-dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "editable": true, 16 | "gnetId": null, 17 | "graphTooltip": 0, 18 | "id": 1, 19 | "links": [], 20 | "panels": [ 21 | { 22 | "aliasColors": {}, 23 | "bars": false, 24 | "dashLength": 10, 25 | "dashes": false, 26 | "datasource": "prometheus", 27 | "fieldConfig": { 28 | "defaults": { 29 | "custom": {} 30 | }, 31 | "overrides": [] 32 | }, 33 | "fill": 1, 34 | "fillGradient": 0, 35 | "gridPos": { 36 | "h": 8, 37 | "w": 11, 38 | "x": 0, 39 | "y": 0 40 | }, 41 | "hiddenSeries": false, 42 | "id": 4, 43 | "legend": { 44 | "avg": false, 45 | "current": false, 46 | "max": false, 47 | "min": false, 48 | "show": true, 49 | "total": false, 50 | "values": false 51 | }, 52 | "lines": true, 53 | "linewidth": 1, 54 | "nullPointMode": "null", 55 | "percentage": false, 56 | "pluginVersion": "7.1.3", 57 | "pointradius": 2, 58 | "points": false, 59 | "renderer": "flot", 60 | "seriesOverrides": [], 61 | "spaceLength": 10, 62 | "stack": false, 63 | "steppedLine": false, 64 | "targets": [ 65 | { 66 | "expr": "Connected_Clients", 67 | "interval": "", 68 | "legendFormat": "", 69 | "refId": "A" 70 | } 71 | ], 72 | "thresholds": [], 73 | "timeFrom": null, 74 | "timeRegions": [], 75 | "timeShift": null, 76 | "title": "Connected Clients", 77 | "tooltip": { 78 | "shared": true, 79 | "sort": 0, 80 | "value_type": "individual" 81 | }, 82 | "type": "graph", 83 | "xaxis": { 84 | "buckets": null, 85 | "mode": "time", 86 | "name": null, 87 | "show": true, 88 | "values": [] 89 | }, 90 | "yaxes": [ 91 | { 92 | "format": "short", 93 | "label": null, 94 | "logBase": 1, 95 | "max": null, 96 | "min": null, 97 | "show": true 98 | }, 99 | { 100 | "format": "short", 101 | "label": null, 102 | "logBase": 1, 103 | "max": null, 104 | "min": null, 105 | "show": true 106 | } 107 | ], 108 | "yaxis": { 109 | "align": false, 110 | "alignLevel": null 111 | } 112 | }, 113 | { 114 | "aliasColors": {}, 115 | "bars": false, 116 | "dashLength": 10, 117 | "dashes": false, 118 | "datasource": "prometheus", 119 | "fieldConfig": { 120 | "defaults": { 121 | "custom": {} 122 | }, 123 | "overrides": [] 124 | }, 125 | "fill": 1, 126 | "fillGradient": 0, 127 | "gridPos": { 128 | "h": 8, 129 | "w": 12, 130 | "x": 11, 131 | "y": 0 132 | }, 133 | "hiddenSeries": false, 134 | "id": 8, 135 | "legend": { 136 | "avg": false, 137 | "current": false, 138 | "max": false, 139 | "min": false, 140 | "show": true, 141 | "total": false, 142 | "values": false 143 | }, 144 | "lines": true, 145 | "linewidth": 1, 146 | "nullPointMode": "null", 147 | "percentage": false, 148 | "pluginVersion": "7.1.3", 149 | "pointradius": 2, 150 | "points": false, 151 | "renderer": "flot", 152 | "seriesOverrides": [], 153 | "spaceLength": 10, 154 | "stack": false, 155 | "steppedLine": false, 156 | "targets": [ 157 | { 158 | "expr": "rate(Message_Sizes_count[10s])/10", 159 | "hide": false, 160 | "interval": "", 161 | "legendFormat": "", 162 | "refId": "A" 163 | } 164 | ], 165 | "thresholds": [], 166 | "timeFrom": null, 167 | "timeRegions": [], 168 | "timeShift": null, 169 | "title": "Messages/sec", 170 | "tooltip": { 171 | "shared": true, 172 | "sort": 0, 173 | "value_type": "individual" 174 | }, 175 | "type": "graph", 176 | "xaxis": { 177 | "buckets": null, 178 | "mode": "time", 179 | "name": null, 180 | "show": true, 181 | "values": [] 182 | }, 183 | "yaxes": [ 184 | { 185 | "format": "short", 186 | "label": null, 187 | "logBase": 1, 188 | "max": null, 189 | "min": null, 190 | "show": true 191 | }, 192 | { 193 | "format": "short", 194 | "label": null, 195 | "logBase": 1, 196 | "max": null, 197 | "min": null, 198 | "show": true 199 | } 200 | ], 201 | "yaxis": { 202 | "align": false, 203 | "alignLevel": null 204 | } 205 | }, 206 | { 207 | "aliasColors": {}, 208 | "bars": false, 209 | "dashLength": 10, 210 | "dashes": false, 211 | "datasource": "prometheus", 212 | "fieldConfig": { 213 | "defaults": { 214 | "custom": {} 215 | }, 216 | "overrides": [] 217 | }, 218 | "fill": 1, 219 | "fillGradient": 0, 220 | "gridPos": { 221 | "h": 8, 222 | "w": 11, 223 | "x": 0, 224 | "y": 8 225 | }, 226 | "hiddenSeries": false, 227 | "id": 2, 228 | "legend": { 229 | "avg": false, 230 | "current": false, 231 | "max": false, 232 | "min": false, 233 | "show": true, 234 | "total": false, 235 | "values": false 236 | }, 237 | "lines": true, 238 | "linewidth": 1, 239 | "nullPointMode": "null", 240 | "percentage": false, 241 | "pluginVersion": "7.1.3", 242 | "pointradius": 2, 243 | "points": false, 244 | "renderer": "flot", 245 | "seriesOverrides": [], 246 | "spaceLength": 10, 247 | "stack": false, 248 | "steppedLine": false, 249 | "targets": [ 250 | { 251 | "expr": "Addressable_Count", 252 | "interval": "", 253 | "legendFormat": "", 254 | "refId": "A" 255 | } 256 | ], 257 | "thresholds": [], 258 | "timeFrom": null, 259 | "timeRegions": [], 260 | "timeShift": null, 261 | "title": "Addressables", 262 | "tooltip": { 263 | "shared": true, 264 | "sort": 0, 265 | "value_type": "individual" 266 | }, 267 | "type": "graph", 268 | "xaxis": { 269 | "buckets": null, 270 | "mode": "time", 271 | "name": null, 272 | "show": true, 273 | "values": [] 274 | }, 275 | "yaxes": [ 276 | { 277 | "format": "short", 278 | "label": null, 279 | "logBase": 1, 280 | "max": null, 281 | "min": null, 282 | "show": true 283 | }, 284 | { 285 | "format": "short", 286 | "label": null, 287 | "logBase": 1, 288 | "max": null, 289 | "min": null, 290 | "show": true 291 | } 292 | ], 293 | "yaxis": { 294 | "align": false, 295 | "alignLevel": null 296 | } 297 | }, 298 | { 299 | "aliasColors": {}, 300 | "bars": false, 301 | "dashLength": 10, 302 | "dashes": false, 303 | "datasource": "prometheus", 304 | "fieldConfig": { 305 | "defaults": { 306 | "custom": {} 307 | }, 308 | "overrides": [] 309 | }, 310 | "fill": 1, 311 | "fillGradient": 0, 312 | "gridPos": { 313 | "h": 8, 314 | "w": 12, 315 | "x": 11, 316 | "y": 8 317 | }, 318 | "hiddenSeries": false, 319 | "id": 10, 320 | "legend": { 321 | "avg": false, 322 | "current": false, 323 | "max": false, 324 | "min": false, 325 | "show": true, 326 | "total": false, 327 | "values": false 328 | }, 329 | "lines": true, 330 | "linewidth": 1, 331 | "nullPointMode": "null", 332 | "percentage": false, 333 | "pluginVersion": "7.1.3", 334 | "pointradius": 2, 335 | "points": false, 336 | "renderer": "flot", 337 | "seriesOverrides": [], 338 | "spaceLength": 10, 339 | "stack": false, 340 | "steppedLine": false, 341 | "targets": [ 342 | { 343 | "expr": "rate(Tick_Timer_seconds_sum[10s])/10*1000", 344 | "interval": "", 345 | "legendFormat": "", 346 | "refId": "A" 347 | } 348 | ], 349 | "thresholds": [], 350 | "timeFrom": null, 351 | "timeRegions": [], 352 | "timeShift": null, 353 | "title": "Tick Timer in ms", 354 | "tooltip": { 355 | "shared": true, 356 | "sort": 0, 357 | "value_type": "individual" 358 | }, 359 | "type": "graph", 360 | "xaxis": { 361 | "buckets": null, 362 | "mode": "time", 363 | "name": null, 364 | "show": true, 365 | "values": [] 366 | }, 367 | "yaxes": [ 368 | { 369 | "format": "short", 370 | "label": null, 371 | "logBase": 1, 372 | "max": null, 373 | "min": null, 374 | "show": true 375 | }, 376 | { 377 | "format": "short", 378 | "label": null, 379 | "logBase": 1, 380 | "max": null, 381 | "min": null, 382 | "show": true 383 | } 384 | ], 385 | "yaxis": { 386 | "align": false, 387 | "alignLevel": null 388 | } 389 | }, 390 | { 391 | "aliasColors": {}, 392 | "bars": false, 393 | "dashLength": 10, 394 | "dashes": false, 395 | "datasource": "prometheus", 396 | "fieldConfig": { 397 | "defaults": { 398 | "custom": {} 399 | }, 400 | "overrides": [] 401 | }, 402 | "fill": 1, 403 | "fillGradient": 0, 404 | "gridPos": { 405 | "h": 8, 406 | "w": 12, 407 | "x": 11, 408 | "y": 16 409 | }, 410 | "hiddenSeries": false, 411 | "id": 6, 412 | "legend": { 413 | "avg": false, 414 | "current": false, 415 | "max": false, 416 | "min": false, 417 | "show": true, 418 | "total": false, 419 | "values": false 420 | }, 421 | "lines": true, 422 | "linewidth": 1, 423 | "nullPointMode": "null", 424 | "percentage": false, 425 | "pluginVersion": "7.1.3", 426 | "pointradius": 2, 427 | "points": false, 428 | "renderer": "flot", 429 | "seriesOverrides": [], 430 | "spaceLength": 10, 431 | "stack": false, 432 | "steppedLine": false, 433 | "targets": [ 434 | { 435 | "expr": "rate(Retry_Attempts_total[10s])", 436 | "interval": "", 437 | "legendFormat": "", 438 | "refId": "A" 439 | }, 440 | { 441 | "expr": "Retry_Errors_total", 442 | "interval": "", 443 | "legendFormat": "", 444 | "refId": "B" 445 | } 446 | ], 447 | "thresholds": [], 448 | "timeFrom": null, 449 | "timeRegions": [], 450 | "timeShift": null, 451 | "title": "Retries", 452 | "tooltip": { 453 | "shared": true, 454 | "sort": 0, 455 | "value_type": "individual" 456 | }, 457 | "type": "graph", 458 | "xaxis": { 459 | "buckets": null, 460 | "mode": "time", 461 | "name": null, 462 | "show": true, 463 | "values": [] 464 | }, 465 | "yaxes": [ 466 | { 467 | "format": "short", 468 | "label": null, 469 | "logBase": 1, 470 | "max": null, 471 | "min": null, 472 | "show": true 473 | }, 474 | { 475 | "format": "short", 476 | "label": null, 477 | "logBase": 1, 478 | "max": null, 479 | "min": null, 480 | "show": true 481 | } 482 | ], 483 | "yaxis": { 484 | "align": false, 485 | "alignLevel": null 486 | } 487 | } 488 | ], 489 | "refresh": "5s", 490 | "schemaVersion": 26, 491 | "style": "dark", 492 | "tags": [], 493 | "templating": { 494 | "list": [] 495 | }, 496 | "time": { 497 | "from": "now-5m", 498 | "to": "now" 499 | }, 500 | "timepicker": { 501 | "refresh_intervals": [ 502 | "10s", 503 | "30s", 504 | "1m", 505 | "5m", 506 | "15m", 507 | "30m", 508 | "1h", 509 | "2h", 510 | "1d" 511 | ] 512 | }, 513 | "timezone": "", 514 | "title": "Orbit Dashboard", 515 | "uid": "Ue5ec8GMz", 516 | "version": 4 517 | } --------------------------------------------------------------------------------