├── .github └── workflows │ └── tests.yaml ├── .gitignore ├── .gitlab └── issue_templates │ └── kind_bug.md ├── .gitmodules ├── .readthedocs.yaml ├── LICENSE ├── Makefile ├── README.md ├── benchmark ├── benchmark_test.go └── testdata │ ├── gen_proto.bat │ ├── gen_proto.sh │ ├── test.pb.go │ └── test.proto ├── cmd ├── repl.go └── root.go ├── deploy └── kubernetes │ ├── example-backend.yaml │ └── example-frontend.yaml ├── docs ├── API.md ├── Makefile ├── builder.md ├── cli.md ├── communication.md ├── conf.py ├── configuration.rst ├── examples.md ├── features.md ├── handshake-validators.md ├── index.rst ├── make.bat ├── overview.md ├── requirements.txt └── tracing.md ├── e2e └── e2e_test.go ├── examples ├── demo │ ├── chat │ │ ├── README.md │ │ ├── main.go │ │ └── web │ │ │ ├── index.html │ │ │ ├── protocol.js │ │ │ └── starx-wsclient.js │ ├── cluster │ │ ├── main.go │ │ └── services │ │ │ ├── connector.go │ │ │ └── room.go │ ├── cluster_grpc │ │ ├── main.go │ │ └── services │ │ │ ├── connector.go │ │ │ └── room.go │ ├── custom_metrics │ │ ├── README.md │ │ ├── config.yaml │ │ ├── main.go │ │ ├── messages │ │ │ ├── args.go │ │ │ └── responses.go │ │ └── services │ │ │ └── room.go │ ├── pipeline │ │ └── main.go │ ├── protos │ │ ├── cluster.pb.go │ │ └── cluster.proto │ ├── rate_limiting │ │ ├── main.go │ │ └── services │ │ │ └── room.go │ └── worker │ │ ├── main.go │ │ ├── protos │ │ ├── arg.pb.go │ │ ├── arg.proto │ │ ├── response.pb.go │ │ └── response.proto │ │ └── services │ │ ├── metagame.go │ │ ├── room.go │ │ └── worker.go └── testing │ ├── docker-compose-jaeger.yml │ ├── docker-compose.yml │ ├── docker │ └── Dockerfile │ ├── main.go │ └── protos │ ├── cluster.pb.go │ └── cluster.proto ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── main.go ├── pkg ├── acceptor │ ├── acceptor.go │ ├── fixtures │ │ ├── server.crt │ │ └── server.key │ ├── proxyprotowrapper.go │ ├── tcp_acceptor.go │ ├── tcp_acceptor_test.go │ ├── ws_acceptor.go │ └── ws_acceptor_test.go ├── acceptorwrapper │ ├── base.go │ ├── base_test.go │ ├── rate_limiter.go │ ├── rate_limiter_test.go │ ├── rate_limiting_wrapper.go │ ├── rate_limiting_wrapper_test.go │ └── wrapper.go ├── agent │ ├── agent.go │ ├── agent_remote.go │ ├── agent_remote_test.go │ ├── agent_test.go │ └── mocks │ │ └── agent.go ├── app.go ├── app_test.go ├── builder.go ├── builder_test.go ├── client │ ├── client.go │ ├── client_test.go │ ├── pitayaclient.go │ └── protoclient.go ├── cluster │ ├── cluster.go │ ├── etcd_service_discovery.go │ ├── etcd_service_discovery_test.go │ ├── grpc_rpc_client.go │ ├── grpc_rpc_client_test.go │ ├── grpc_rpc_server.go │ ├── grpc_rpc_server_test.go │ ├── info_retriever.go │ ├── info_retriever_test.go │ ├── mocks │ │ ├── cluster.go │ │ └── service_discovery.go │ ├── nats_rpc_client.go │ ├── nats_rpc_client_test.go │ ├── nats_rpc_common.go │ ├── nats_rpc_common_test.go │ ├── nats_rpc_server.go │ ├── nats_rpc_server_test.go │ ├── server.go │ ├── server_test.go │ └── service_discovery.go ├── component.go ├── component │ ├── base.go │ ├── component.go │ ├── method.go │ ├── method_test.go │ ├── options.go │ ├── options_test.go │ ├── service.go │ └── service_test.go ├── component_test.go ├── config │ ├── config.go │ ├── config_test.go │ └── viper_config.go ├── conn │ ├── codec │ │ ├── constants.go │ │ ├── mocks │ │ │ ├── packet_decoder.go │ │ │ └── packet_encoder.go │ │ ├── packet_decoder.go │ │ ├── packet_encoder.go │ │ ├── pomelo_packet_decoder.go │ │ ├── pomelo_packet_decoder_test.go │ │ ├── pomelo_packet_encoder.go │ │ ├── pomelo_packet_encoder_test.go │ │ └── utils.go │ ├── message │ │ ├── fixtures │ │ │ ├── test_invalid_message.golden │ │ │ ├── test_must_gzip.golden │ │ │ ├── test_notify_type.golden │ │ │ ├── test_notify_type_compressed.golden │ │ │ ├── test_push_type.golden │ │ │ ├── test_push_type_compressed.golden │ │ │ ├── test_reponse_type.golden │ │ │ ├── test_reponse_type_with_data.golden │ │ │ ├── test_reponse_type_with_error.golden │ │ │ ├── test_reponse_type_with_id.golden │ │ │ ├── test_request_type.golden │ │ │ ├── test_request_type_compressed.golden │ │ │ └── test_wrong_type.golden │ │ ├── message.go │ │ ├── message_encoder.go │ │ ├── message_test.go │ │ └── mocks │ │ │ └── message_encoder.go │ └── packet │ │ ├── constants.go │ │ ├── packet.go │ │ └── packet_test.go ├── constants │ ├── const.go │ ├── errors.go │ └── version.go ├── context │ ├── context.go │ ├── context_test.go │ └── fixtures │ │ ├── one_element.golden │ │ └── registered_struct.golden ├── defaultpipelines │ ├── default_struct_validator.go │ ├── default_struct_validator_test.go │ └── struct_validator.go ├── docgenerator │ ├── descriptors.go │ ├── descriptors_test.go │ ├── generator.go │ └── generator_test.go ├── errors │ ├── errors.go │ └── errors_test.go ├── group.go ├── group_test.go ├── groups │ ├── etcd_group_service.go │ ├── etcd_group_service_test.go │ ├── group_service.go │ ├── group_service_test.go │ ├── memory_group_service.go │ └── memory_group_service_test.go ├── helpers │ ├── helpers.go │ └── type_support.go ├── interfaces │ ├── interfaces.go │ └── mocks │ │ └── interfaces.go ├── kick.go ├── kick_test.go ├── logger │ ├── interfaces │ │ └── interfaces.go │ ├── logger.go │ ├── logger_test.go │ ├── logrus │ │ └── logrus.go │ └── test │ │ └── test.go ├── metrics │ ├── constants.go │ ├── mocks │ │ ├── reporter.go │ │ └── statsd_reporter.go │ ├── models │ │ └── models.go │ ├── prometheus_reporter.go │ ├── report.go │ ├── report_test.go │ ├── reporter_interfaces.go │ ├── statsd_reporter.go │ └── statsd_reporter_test.go ├── mocks │ ├── acceptor.go │ └── app.go ├── module.go ├── module_test.go ├── modules │ ├── api_docs_gen.go │ ├── base.go │ ├── binary.go │ ├── binary_test.go │ ├── binary_windows.go │ ├── binding_storage.go │ └── unique_session.go ├── networkentity │ ├── mocks │ │ └── networkentity.go │ └── networkentity.go ├── pipeline │ ├── pipeline.go │ └── pipeline_test.go ├── protos │ ├── bind.pb.go │ ├── doc.pb.go │ ├── docmsg.pb.go │ ├── error.pb.go │ ├── kick.pb.go │ ├── mocks │ │ └── pitaya.go │ ├── msg.pb.go │ ├── pitaya.pb.go │ ├── protodescriptor.pb.go │ ├── push.pb.go │ ├── request.pb.go │ ├── response.pb.go │ ├── session.pb.go │ └── test │ │ ├── somestruct.pb.go │ │ ├── testrequest.pb.go │ │ └── testresponse.pb.go ├── push.go ├── push_test.go ├── remote │ ├── sys.go │ └── sys_test.go ├── reporters.go ├── route │ ├── route.go │ └── route_test.go ├── router │ ├── router.go │ └── router_test.go ├── rpc.go ├── rpc_test.go ├── serialize │ ├── json │ │ ├── json.go │ │ └── json_test.go │ ├── mocks │ │ └── serializer.go │ ├── protobuf │ │ ├── fixtures │ │ │ └── TestMarshal │ │ │ │ └── test_ok.golden │ │ ├── protobuf.go │ │ └── protobuf_test.go │ └── serializer.go ├── service │ ├── base_service.go │ ├── fixtures │ │ ├── unmarshal_remote_test_1.golden │ │ ├── unmarshal_remote_test_2.golden │ │ └── unmarshal_remote_test_3.golden │ ├── handler.go │ ├── handler_pool.go │ ├── handler_pool_test.go │ ├── handler_test.go │ ├── remote.go │ ├── remote_test.go │ ├── util.go │ └── util_test.go ├── session │ ├── fixtures │ │ ├── testSessionSetData_1.golden │ │ ├── testSessionSetData_2.golden │ │ ├── testSessionSetData_3.golden │ │ ├── testSessionSetData_4.golden │ │ ├── testSessionSetData_5.golden │ │ ├── testUpdateEncodedData_1.golden │ │ ├── testUpdateEncodedData_2.golden │ │ ├── testUpdateEncodedData_3.golden │ │ ├── testUpdateEncodedData_4.golden │ │ └── testUpdateEncodedData_5.golden │ ├── mocks │ │ └── session.go │ ├── session.go │ ├── session_test.go │ ├── static.go │ └── test │ │ └── static_test.go ├── static.go ├── static_test.go ├── timer.go ├── timer │ ├── timer.go │ └── timer_test.go ├── timer_test.go ├── tracing │ ├── otel.go │ ├── span.go │ └── span_test.go ├── util │ ├── compression │ │ ├── compression.go │ │ ├── compression_test.go │ │ └── fixtures │ │ │ ├── compression_deflate_test_1.golden │ │ │ ├── compression_deflate_test_2.golden │ │ │ └── compression_deflate_test_3.golden │ ├── fixtures │ │ ├── gob_encode_test_1.golden │ │ ├── gob_encode_test_2.golden │ │ └── gob_encode_test_3.golden │ ├── util.go │ └── util_test.go └── worker │ ├── constants.go │ ├── mocks │ └── rpc_job.go │ ├── models.go │ ├── report.go │ ├── report_test.go │ ├── rpc_job.go │ ├── worker.go │ └── worker_test.go ├── repl ├── commands.go ├── file.go ├── helpers.go ├── log.go ├── main.go └── shell.go └── xk6-pitaya ├── Dockerfile ├── LICENSE ├── README.md ├── client.go ├── examples └── scenario1.js ├── go.mod ├── go.sum ├── module.go ├── register.go └── stats.go /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - v2 8 | pull_request: 9 | branches: 10 | - main 11 | - v2 12 | 13 | jobs: 14 | deps: 15 | name: Dependencies 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: '1.23' 24 | - name: Download dependencies 25 | run: go mod download 26 | unit-test: 27 | name: Unit Test 28 | runs-on: ubuntu-latest 29 | needs: deps 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | - name: Set up Go 34 | uses: actions/setup-go@v5 35 | with: 36 | go-version: '1.23' 37 | - name: Setup dependencies 38 | env: 39 | GO111MODULE: auto 40 | run: make setup-ci 41 | - name: Run tests 42 | run: make test-coverage 43 | - name: Send coverage 44 | env: 45 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | run: ~/go/bin/goveralls -coverprofile=coverprofile.out -service=github 47 | e2e-test-nats: 48 | name: Nats Test End to End 49 | runs-on: ubuntu-latest 50 | needs: deps 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v4 54 | - name: Set up Go 55 | uses: actions/setup-go@v5 56 | with: 57 | go-version: '1.23' 58 | - name: Run tests 59 | run: make e2e-test-nats 60 | e2e-test-grpc: 61 | name: GRPC Test End to End 62 | runs-on: ubuntu-latest 63 | needs: deps 64 | steps: 65 | - name: Checkout 66 | uses: actions/checkout@v4 67 | - name: Set up Go 68 | uses: actions/setup-go@v5 69 | with: 70 | go-version: '1.23' 71 | - name: Run tests 72 | run: make e2e-test-grpc 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.PID 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | .DS_Store 15 | 16 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 17 | .glide/ 18 | 19 | .idea/ 20 | vendor 21 | debug 22 | .vscode 23 | 24 | # vim temp files 25 | *.swp 26 | *.swo 27 | 28 | examples/testing/server 29 | examples/demo/cluster_grpc/main 30 | -------------------------------------------------------------------------------- /.gitlab/issue_templates/kind_bug.md: -------------------------------------------------------------------------------- 1 | 4 | ## Versions 5 | 6 | * Pitaya: 7 | * LibPitaya (if relevant): 8 | 9 | ## Summary 10 | 11 | 16 | 17 | ## Steps to reproduce 18 | 19 | 22 | 23 | ## Suggestion 24 | 25 | 28 | 29 | 30 | /label ~"kind/bug" 31 | 32 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "protos"] 2 | path = pitaya-protos 3 | url = https://github.com/topfreegames/pitaya-protos 4 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | # python: "3.12" 13 | # You can also specify other tool versions: 14 | # nodejs: "20" 15 | # rust: "1.70" 16 | golang: "1.23" 17 | 18 | # Build documentation in the "docs/" directory with Sphinx 19 | sphinx: 20 | configuration: docs/conf.py 21 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 22 | # builder: "dirhtml" 23 | # Fail on all warnings to avoid broken references 24 | # fail_on_warning: true 25 | 26 | # Optionally build your docs in additional formats such as PDF and ePub 27 | # formats: 28 | # - pdf 29 | # - epub 30 | 31 | # Optional but recommended, declare the Python requirements required 32 | # to build your documentation 33 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 34 | python: 35 | install: 36 | - requirements: docs/requirements.txt 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 TFG Co and nano Authors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /benchmark/testdata/gen_proto.bat: -------------------------------------------------------------------------------- 1 | protoc --gogofaster_out=. *.proto 2 | -------------------------------------------------------------------------------- /benchmark/testdata/gen_proto.sh: -------------------------------------------------------------------------------- 1 | protoc --go_out . *.proto 2 | -------------------------------------------------------------------------------- /benchmark/testdata/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package testdata; 3 | 4 | message Ping { 5 | string Content = 1; 6 | } 7 | 8 | message Pong { 9 | string Content = 2; 10 | } -------------------------------------------------------------------------------- /cmd/repl.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Wildlife Studios 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package cmd 17 | 18 | import ( 19 | "github.com/spf13/cobra" 20 | "github.com/topfreegames/pitaya/v3/repl" 21 | ) 22 | 23 | var docsRoute string 24 | var fileName string 25 | var prettyJSON bool 26 | 27 | // replCmd opens pitaya REPL tool 28 | var replCmd = &cobra.Command{ 29 | Use: "repl", 30 | Short: "starts pitaya repl tool", 31 | Long: `starts pitaya repl tool`, 32 | Run: func(cmd *cobra.Command, args []string) { 33 | repl.Start(docsRoute, fileName, prettyJSON) 34 | }, 35 | } 36 | 37 | func init() { 38 | replCmd.Flags().StringVarP(&docsRoute, "docs", "d", "", "route containing the documentation") 39 | replCmd.Flags().StringVarP(&fileName, "filename", "f", "", "file containing the commands to run") 40 | replCmd.Flags().BoolVarP(&prettyJSON, "pretty", "p", false, "print pretty jsons") 41 | rootCmd.AddCommand(replCmd) 42 | } 43 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Wildlife Studios 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package cmd 17 | 18 | import ( 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | var cfgFile string 23 | 24 | // rootCmd represents the base command when called without any subcommands 25 | var rootCmd = &cobra.Command{ 26 | Use: "pitaya", 27 | Short: "Pitaya command line utilities", 28 | Long: `Pitaya command line utilities`, 29 | // Uncomment the following line if your bare application 30 | // has an action associated with it: 31 | // Run: func(cmd *cobra.Command, args []string) { }, 32 | } 33 | 34 | // Execute adds all child commands to the root command and sets flags appropriately. 35 | // This is called by main.main(). It only needs to happen once to the rootCmd. 36 | func Execute() { 37 | cobra.CheckErr(rootCmd.Execute()) 38 | } 39 | 40 | func init() { 41 | } 42 | -------------------------------------------------------------------------------- /deploy/kubernetes/example-backend.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: pitaya-example 6 | name: backend 7 | namespace: pitaya-example 8 | spec: 9 | replicas: 3 10 | selector: 11 | matchLabels: 12 | app: pitaya-example 13 | template: 14 | metadata: 15 | labels: 16 | app: pitaya-example 17 | server: backend 18 | spec: 19 | containers: 20 | - image: quay.io/felipejfc/pitaya-example-sv:v1 21 | command: 22 | - /server 23 | - -type 24 | - game 25 | - -frontend=false 26 | env: 27 | - name: PITAYA_CLUSTER_RPC_CLIENT_NATS_CONNECT 28 | value: nats://nats-cluster-1.nats-io.svc.cluster.local:4222 29 | - name: PITAYA_CLUSTER_RPC_SERVER_NATS_CONNECT 30 | value: nats://nats-cluster-1.nats-io.svc.cluster.local:4222 31 | - name: PITAYA_CLUSTER_SD_ETCD_ENDPOINTS 32 | value: etcd-cluster-1.etcd.svc.cluster.local:2379 33 | - name: PITAYA_HEARTBEAT_INTERVAL 34 | value: 10s 35 | name: backend 36 | resources: 37 | limits: 38 | cpu: 1000m 39 | memory: 300Mi 40 | requests: 41 | cpu: 100m 42 | memory: 200Mi 43 | -------------------------------------------------------------------------------- /deploy/kubernetes/example-frontend.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: pitaya-example 6 | name: frontend 7 | namespace: pitaya-example 8 | spec: 9 | replicas: 3 10 | selector: 11 | matchLabels: 12 | app: pitaya-example 13 | template: 14 | metadata: 15 | labels: 16 | app: pitaya-example 17 | server: frontend 18 | spec: 19 | containers: 20 | - image: quay.io/felipejfc/pitaya-example-sv:v1 21 | command: 22 | - /server 23 | - -type 24 | - connector 25 | - -frontend=true 26 | env: 27 | - name: PITAYA_CLUSTER_RPC_CLIENT_NATS_CONNECT 28 | value: nats://nats-cluster-1.nats-io.svc.cluster.local:4222 29 | - name: PITAYA_CLUSTER_RPC_SERVER_NATS_CONNECT 30 | value: nats://nats-cluster-1.nats-io.svc.cluster.local:4222 31 | - name: PITAYA_CLUSTER_SD_ETCD_ENDPOINTS 32 | value: etcd-cluster-1.etcd.svc.cluster.local:2379 33 | - name: PITAYA_HEARTBEAT_INTERVAL 34 | value: 10s 35 | name: frontend 36 | resources: 37 | limits: 38 | cpu: 1000m 39 | memory: 300Mi 40 | requests: 41 | cpu: 100m 42 | memory: 200Mi 43 | --- 44 | apiVersion: v1 45 | kind: Service 46 | metadata: 47 | labels: 48 | app: pitaya-example 49 | name: frontend 50 | namespace: pitaya-example 51 | spec: 52 | ports: 53 | - name: tcp 54 | port: 32222 55 | protocol: TCP 56 | targetPort: 32222 57 | selector: 58 | app: pitaya-example 59 | server: frontend 60 | type: LoadBalancer 61 | 62 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = Pitaya 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | -------------------------------------------------------------------------------- /docs/builder.md: -------------------------------------------------------------------------------- 1 | Builder 2 | === 3 | 4 | Pitaya offers a [`Builder`](../builder.go) object which can be utilized to define a sort of pitaya properties. 5 | 6 | ### PostBuildHooks 7 | 8 | Post-build hooks can be used to execute additional actions automatically after the build process. It also allows you to interact with the built pitaya app. 9 | 10 | A common use case is where it becomes necessary to perform configuration steps in both the pitaya builder and the pitaya app being built. In such cases, an effective approach is to internalize these configurations, enabling you to handle them collectively in a single operation or process. It simplifies the overall configuration process, reducing the need for separate and potentially repetitive steps. 11 | 12 | ```go 13 | // main.go 14 | cfg := config.NewDefaultBuilderConfig() 15 | builder := pitaya.NewDefaultBuilder(isFrontEnd, "my-server-type", pitaya.Cluster, map[string]string{}, *cfg) 16 | 17 | customModule := NewCustomModule(builder) 18 | customModule.ConfigurePitaya(builder) 19 | 20 | app := builder.Build() 21 | 22 | // custom_object.go 23 | type CustomObject struct { 24 | builder *pitaya.Builder 25 | } 26 | 27 | func NewCustomObject(builder *pitaya.Builder) *CustomObject { 28 | return &CustomObject{ 29 | builder: builder, 30 | } 31 | } 32 | 33 | func (object *CustomObject) ConfigurePitaya() { 34 | object.builder.AddAcceptor(...) 35 | object.builder.AddPostBuildHook(func (app pitaya.App) { 36 | app.Register(...) 37 | }) 38 | } 39 | ``` 40 | 41 | In the above example the `ConfigurePitaya` method of the `CustomObject` is adding an `Acceptor` to the pitaya app being built, and also adding a post-build function which will register a handler `Component` that will expose endpoints to receive calls. 42 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | Example projects can be found [here](https://github.com/topfreegames/pitaya/tree/master/examples/demo) 5 | 6 | -------------------------------------------------------------------------------- /docs/handshake-validators.md: -------------------------------------------------------------------------------- 1 | Handshake Validators 2 | ===== 3 | 4 | Pitaya allows to defined Handshake Validators.
5 | 6 | The primary purpose of these validators is to perform validation checks on the data transmitted by the client. The validators play a crucial role in verifying the integrity and reliability of the client's input before establishing a connection. 7 | 8 | In addition to data validation, handshake validators can also execute other custom logic to assess the client's compliance with the server-defined requirements. This additional logic may involve evaluating factors such as authenticating credentials, permissions, or any other criteria necessary to determine the client's eligibility to access the server. 9 | 10 | ### Adding handshake validators 11 | 12 | To ensure the effective utilization of these validators, they should be added to the `SessionPool` component. As a result, each newly created session within the `SessionPool` will automatically incorporate the designated validators. 13 | 14 | Once the handshake process is initiated, the validators will be invoked to execute their validation routines. 15 | 16 | ```go 17 | cfg := config.NewDefaultBuilderConfig() 18 | builder := pitaya.NewDefaultBuilder(isFrontEnd, "my-server-type", pitaya.Cluster, map[string]string{}, *cfg) 19 | builder.SessionPool.AddHandshakeValidator("MyCustomValidator", func (data *session.HandshakeData) error { 20 | if data.Sys.Version != "1.0.0" { 21 | return errors.New("Unknown client version") 22 | } 23 | 24 | return nil 25 | }) 26 | ``` 27 | 28 | As a result of the validation process, if an error is encountered, the server will transmit a message to client within the code 400. This code emulates the widely recognized HTTP Bad Request status code, indicating that the client's request could not be fulfilled due to invalid data. Otherwise, if the validation process succeeds, the server will dispatch a message to client containing a code 200, mirroring the HTTP Ok status code.
29 | **Is important to mention that, when there are many validator functions, the validation will stop as soon it encounters the first error.** 30 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Pitaya documentation master file, created by 2 | sphinx-quickstart on Mon Jul 16 17:01:02 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Pitaya's documentation! 7 | ================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | overview 14 | features 15 | communication 16 | configuration 17 | API 18 | cli 19 | examples 20 | tracing 21 | 22 | 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=Pitaya 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-markdown-tables==0.0.17 2 | recommonmark==0.7.1 3 | sphinx-rtd-theme==2.0.0 4 | myst-parser==3.0.1 5 | -------------------------------------------------------------------------------- /docs/tracing.md: -------------------------------------------------------------------------------- 1 | Tracing 2 | ======= 3 | 4 | Pitaya supports tracing using [OpenTelemetry](https://opentelemetry.io/). 5 | 6 | ### Using OpenTelemetry tracing 7 | 8 | Pitaya supports tracing using [OpenTelemetry](https://opentelemetry.io/). To enable and configure OpenTelemetry tracing, you can use standard OpenTelemetry environment variables. 9 | 10 | First, make sure to call the `InitializeOtel` function in your main application: 11 | 12 | ```go 13 | func main() { 14 | // ... 15 | err := tracing.InitializeOtel() 16 | if err != nil { 17 | logger.Log.Errorf("Failed to initialize OpenTelemetry: %v", err) 18 | } 19 | // ... 20 | } 21 | ``` 22 | 23 | ### Configuration Options 24 | 25 | OpenTelemetry can be configured using standard environment variables. Here are some key variables you might want to set: 26 | 27 | - `OTEL_SERVICE_NAME`: The name of your service. 28 | - `OTEL_EXPORTER_OTLP_ENDPOINT`: The endpoint of your OpenTelemetry collector. 29 | - `OTEL_EXPORTER_OTLP_PROTOCOL`: The protocol to use (e.g., `grpc` or `http/protobuf`). 30 | - `OTEL_TRACES_SAMPLER`: The sampling strategy to use. 31 | - `OTEL_TRACES_SAMPLER_ARG`: The argument for the sampling strategy. 32 | - `OTEL_SDK_DISABLED`: Set to `true` to disable tracing. 33 | 34 | For a complete list of OpenTelemetry environment variables, refer to the [OpenTelemetry specification](https://opentelemetry.io/docs/concepts/sdk-configuration/general-sdk-configuration/). 35 | 36 | ### Testing Locally 37 | 38 | To test OpenTelemetry tracing locally, you can use Jaeger as your tracing backend. First, start Jaeger using Docker: 39 | 40 | ```bash 41 | make run-jaeger-aio 42 | make run-cluster-example-frontend-tracing 43 | make run-cluster-example-backend-tracing 44 | ``` 45 | 46 | The last two commands will run your Pitaya servers with OpenTelemetry configured with the following envs: 47 | 48 | ```bash 49 | OTEL_SERVICE_NAME=example-frontend OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 OTEL_EXPORTER_OTLP_PROTOCOL=grpc OTEL_TRACES_SAMPLER=parentbased_traceidratio OTEL_TRACES_SAMPLER_ARG="1" 50 | 51 | OTEL_SERVICE_NAME=example-backend OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 OTEL_EXPORTER_OTLP_PROTOCOL=grpc OTEL_TRACES_SAMPLER=parentbased_traceidratio OTEL_TRACES_SAMPLER_ARG="1" 52 | ``` 53 | 54 | Access the Jaeger UI at http://localhost:16686 to view and analyze your traces. -------------------------------------------------------------------------------- /examples/demo/chat/README.md: -------------------------------------------------------------------------------- 1 | # pitaya-chat-demo 2 | chat room demo base on [pitaya](https://github.com/topfreegames/pitaya) in 100 lines 3 | 4 | refs: https://github.com/topfreegames/pitaya 5 | 6 | ## Required 7 | - golang 8 | - websocket 9 | 10 | ## Run 11 | ``` 12 | docker compose -f ../../testing/docker-compose.yml up -d etcd nats 13 | go run main.go 14 | ``` 15 | 16 | open browser => http://localhost:3251/web/ 17 | -------------------------------------------------------------------------------- /examples/demo/chat/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chat Demo 6 | 7 | 8 |
9 | 12 |
13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | 22 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /examples/demo/cluster_grpc/services/connector.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/topfreegames/pitaya/v3/examples/demo/protos" 8 | pitaya "github.com/topfreegames/pitaya/v3/pkg" 9 | "github.com/topfreegames/pitaya/v3/pkg/component" 10 | ) 11 | 12 | // ConnectorRemote is a remote that will receive rpc's 13 | type ConnectorRemote struct { 14 | component.Base 15 | } 16 | 17 | // Connector struct 18 | type Connector struct { 19 | component.Base 20 | app pitaya.Pitaya 21 | } 22 | 23 | // SessionData struct 24 | type SessionData struct { 25 | Data map[string]interface{} 26 | } 27 | 28 | // Response struct 29 | type Response struct { 30 | Code int32 31 | Msg string 32 | } 33 | 34 | // NewConnector ctor 35 | func NewConnector(app pitaya.Pitaya) *Connector { 36 | return &Connector{app: app} 37 | } 38 | 39 | func reply(code int32, msg string) (*Response, error) { 40 | res := &Response{ 41 | Code: code, 42 | Msg: msg, 43 | } 44 | return res, nil 45 | } 46 | 47 | // GetSessionData gets the session data 48 | func (c *Connector) GetSessionData(ctx context.Context) (*SessionData, error) { 49 | s := c.app.GetSessionFromCtx(ctx) 50 | res := &SessionData{ 51 | Data: s.GetData(), 52 | } 53 | return res, nil 54 | } 55 | 56 | // SetSessionData sets the session data 57 | func (c *Connector) SetSessionData(ctx context.Context, data *SessionData) (*Response, error) { 58 | s := c.app.GetSessionFromCtx(ctx) 59 | err := s.SetData(data.Data) 60 | if err != nil { 61 | return nil, pitaya.Error(err, "CN-000", map[string]string{"failed": "set data"}) 62 | } 63 | return reply(200, "success") 64 | } 65 | 66 | // NotifySessionData sets the session data 67 | func (c *Connector) NotifySessionData(ctx context.Context, data *SessionData) { 68 | s := c.app.GetSessionFromCtx(ctx) 69 | err := s.SetData(data.Data) 70 | if err != nil { 71 | fmt.Println("got error on notify", err) 72 | } 73 | } 74 | 75 | // SendPushToUser sends a push to a user 76 | func (c *Connector) SendPushToUser(ctx context.Context, msg *UserMessage) (*Response, error) { 77 | _, err := c.app.SendPushToUsers("onMessage", msg, []string{"2"}, "connector") 78 | if err != nil { 79 | return nil, err 80 | } 81 | return &Response{ 82 | Code: 200, 83 | Msg: "boa", 84 | }, nil 85 | } 86 | 87 | // RemoteFunc is a function that will be called remotely 88 | func (c *ConnectorRemote) RemoteFunc(ctx context.Context, msg *protos.RPCMsg) (*protos.RPCRes, error) { 89 | fmt.Printf("received a remote call with this message: %s\n", msg) 90 | return &protos.RPCRes{ 91 | Msg: fmt.Sprintf("received msg: %s", msg.GetMsg()), 92 | }, nil 93 | } 94 | -------------------------------------------------------------------------------- /examples/demo/custom_metrics/README.md: -------------------------------------------------------------------------------- 1 | Custom Metrics Example 2 | ======================= 3 | 4 | # Run 5 | `make run-custom-metrics-example` 6 | 7 | # Call routes 8 | By using [pitaya-cli](https://github.com/topfreegames/pitaya-cli), call: 9 | ``` 10 | connect localhost:3250 11 | request room.room.setcounter {"value": 1.0, "tag1": "value1", "tag2": "value2"} 12 | request room.room.setgauge1 {"value": 1.0, "tag1": "value1"} 13 | request room.room.setgauge2 {"value": 1.0, "tag2": "value2"} 14 | request room.room.setsummary {"value": 1.0, "tag1": "value1"} 15 | ``` 16 | 17 | Check out the results on `curl localhost:9090/metrics | grep 'my_'` 18 | -------------------------------------------------------------------------------- /examples/demo/custom_metrics/config.yaml: -------------------------------------------------------------------------------- 1 | pitaya: 2 | metrics: 3 | prometheus: 4 | enabled: true 5 | custom: 6 | counters: 7 | - subsystem: room 8 | name: my_counter 9 | help: a counter example 10 | labels: ['tag1', 'tag2'] 11 | gauges: 12 | - subsystem: room 13 | name: my_gauge_1 14 | help: a gauge example 15 | labels: ['tag1'] 16 | - subsystem: room 17 | name: my_gauge_2 18 | help: a gauge example 19 | labels: ['tag2'] 20 | summaries: 21 | - subsystem: room 22 | name: my_summary 23 | help: a summary example 24 | objectives: {0.7: 0.05, 0.9: 0.01, 0.99: 0.001} 25 | labels: ['tag1'] 26 | -------------------------------------------------------------------------------- /examples/demo/custom_metrics/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/spf13/viper" 9 | "github.com/topfreegames/pitaya/v3/examples/demo/custom_metrics/services" 10 | pitaya "github.com/topfreegames/pitaya/v3/pkg" 11 | "github.com/topfreegames/pitaya/v3/pkg/acceptor" 12 | "github.com/topfreegames/pitaya/v3/pkg/component" 13 | "github.com/topfreegames/pitaya/v3/pkg/config" 14 | ) 15 | 16 | var app pitaya.Pitaya 17 | 18 | func main() { 19 | port := flag.Int("port", 3250, "the port to listen") 20 | svType := "room" 21 | isFrontend := true 22 | 23 | flag.Parse() 24 | 25 | cfg := viper.New() 26 | cfg.AddConfigPath(".") 27 | cfg.SetConfigName("config") 28 | err := cfg.ReadInConfig() 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | tcp := acceptor.NewTCPAcceptor(fmt.Sprintf(":%d", *port)) 34 | 35 | conf := config.NewConfig(cfg) 36 | builder := pitaya.NewBuilderWithConfigs(isFrontend, svType, pitaya.Cluster, map[string]string{}, conf) 37 | builder.AddAcceptor(tcp) 38 | app = builder.Build() 39 | 40 | defer app.Shutdown() 41 | 42 | app.Register(services.NewRoom(app), 43 | component.WithName("room"), 44 | component.WithNameFunc(strings.ToLower), 45 | ) 46 | 47 | app.Start() 48 | } 49 | -------------------------------------------------------------------------------- /examples/demo/custom_metrics/messages/args.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | // SetCounterArg is the argument on room.setcounter handler 4 | type SetCounterArg struct { 5 | Value float64 6 | Tag1 string 7 | Tag2 string 8 | } 9 | 10 | // SetGaugeArg is the argument on room.setgauge* handler 11 | type SetGaugeArg struct { 12 | Value float64 13 | Tag string 14 | } 15 | 16 | // SetSummaryArg is the argument on room.setsummary handler 17 | type SetSummaryArg struct { 18 | Value float64 19 | Tag string 20 | } 21 | -------------------------------------------------------------------------------- /examples/demo/custom_metrics/messages/responses.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | // Response is the basic response on handlers 4 | type Response struct { 5 | Code int 6 | } 7 | 8 | // OKResponse returns a response with code 200 9 | func OKResponse() *Response { 10 | return &Response{Code: 200} 11 | } 12 | -------------------------------------------------------------------------------- /examples/demo/custom_metrics/services/room.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/topfreegames/pitaya/v3/examples/demo/custom_metrics/messages" 7 | pitaya "github.com/topfreegames/pitaya/v3/pkg" 8 | "github.com/topfreegames/pitaya/v3/pkg/component" 9 | ) 10 | 11 | // Room server 12 | type Room struct { 13 | component.Base 14 | app pitaya.Pitaya 15 | } 16 | 17 | // NewRoom ctor 18 | func NewRoom(app pitaya.Pitaya) *Room { 19 | return &Room{ 20 | app: app, 21 | } 22 | } 23 | 24 | // SetCounter sets custom my_counter 25 | func (r *Room) SetCounter( 26 | ctx context.Context, 27 | arg *messages.SetCounterArg, 28 | ) (*messages.Response, error) { 29 | counterMetricName := "my_counter" 30 | 31 | for _, reporter := range r.app.GetMetricsReporters() { 32 | reporter.ReportCount(counterMetricName, map[string]string{ 33 | "tag1": arg.Tag1, 34 | "tag2": arg.Tag2, 35 | }, arg.Value) 36 | } 37 | 38 | return messages.OKResponse(), nil 39 | } 40 | 41 | // SetGauge1 sets custom my_gauge_1 42 | func (r *Room) SetGauge1( 43 | ctx context.Context, 44 | arg *messages.SetGaugeArg, 45 | ) (*messages.Response, error) { 46 | counterMetricName := "my_gauge_1" 47 | 48 | for _, reporter := range r.app.GetMetricsReporters() { 49 | reporter.ReportGauge(counterMetricName, map[string]string{ 50 | "tag1": arg.Tag, 51 | }, arg.Value) 52 | } 53 | 54 | return messages.OKResponse(), nil 55 | } 56 | 57 | // SetGauge2 sets custom my_gauge_2 58 | func (r *Room) SetGauge2( 59 | ctx context.Context, 60 | arg *messages.SetGaugeArg, 61 | ) (*messages.Response, error) { 62 | counterMetricName := "my_gauge_2" 63 | 64 | for _, reporter := range r.app.GetMetricsReporters() { 65 | reporter.ReportGauge(counterMetricName, map[string]string{ 66 | "tag2": arg.Tag, 67 | }, arg.Value) 68 | } 69 | 70 | return messages.OKResponse(), nil 71 | } 72 | 73 | // SetSummary sets custom my_summary 74 | func (r *Room) SetSummary( 75 | ctx context.Context, 76 | arg *messages.SetSummaryArg, 77 | ) (*messages.Response, error) { 78 | counterMetricName := "my_summary" 79 | 80 | for _, reporter := range r.app.GetMetricsReporters() { 81 | reporter.ReportSummary(counterMetricName, map[string]string{ 82 | "tag1": arg.Tag, 83 | }, arg.Value) 84 | } 85 | 86 | return messages.OKResponse(), nil 87 | } 88 | -------------------------------------------------------------------------------- /examples/demo/protos/cluster.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package cluster_protos; 4 | option go_package = "examples/demo/protos"; 5 | 6 | // RPCMsg message to be sent using rpc 7 | message RPCMsg { 8 | string Msg = 1; 9 | } 10 | 11 | // RPCRes is the rpc response 12 | message RPCRes { 13 | string Msg = 1; 14 | } 15 | 16 | // UserMessage represents a message that user sent 17 | message UserMessage { 18 | string name = 1; 19 | string content = 2; 20 | } 21 | 22 | // Stats exports the room status 23 | message Stats { 24 | int64 outbound_bytes = 1; 25 | int64 inbound_bytes = 2; 26 | } 27 | 28 | // SendRPCMsg represents a rpc message 29 | message SendRPCMsg { 30 | string server_id = 1; 31 | string route = 2; 32 | string msg = 3; 33 | } 34 | 35 | // NewUser message will be received when new user join room 36 | message NewUser { 37 | string content = 1; 38 | } 39 | 40 | // AllMembers contains all members uid 41 | message AllMembers { 42 | repeated string Members = 1; 43 | } 44 | 45 | // JoinResponse represents the result of joining room 46 | message JoinResponse { 47 | int64 code = 1; 48 | string result = 2; 49 | } 50 | 51 | // Response struct 52 | message Response { 53 | int32 code = 1; 54 | string msg = 2; 55 | } 56 | -------------------------------------------------------------------------------- /examples/demo/rate_limiting/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/spf13/viper" 10 | "github.com/topfreegames/pitaya/v3/examples/demo/rate_limiting/services" 11 | pitaya "github.com/topfreegames/pitaya/v3/pkg" 12 | "github.com/topfreegames/pitaya/v3/pkg/acceptor" 13 | "github.com/topfreegames/pitaya/v3/pkg/acceptorwrapper" 14 | "github.com/topfreegames/pitaya/v3/pkg/component" 15 | "github.com/topfreegames/pitaya/v3/pkg/config" 16 | "github.com/topfreegames/pitaya/v3/pkg/metrics" 17 | ) 18 | 19 | func createAcceptor(port int, reporters []metrics.Reporter) acceptor.Acceptor { 20 | 21 | // 5 requests in 1 minute. Doesn't make sense, just to test 22 | // rate limiting 23 | vConfig := viper.New() 24 | vConfig.Set("pitaya.conn.ratelimiting.limit", 5) 25 | vConfig.Set("pitaya.conn.ratelimiting.interval", time.Minute) 26 | pConfig := config.NewConfig(vConfig) 27 | 28 | rateLimitConfig := config.NewPitayaConfig(pConfig).Conn.RateLimiting 29 | 30 | tcp := acceptor.NewTCPAcceptor(fmt.Sprintf(":%d", port)) 31 | return acceptorwrapper.WithWrappers( 32 | tcp, 33 | acceptorwrapper.NewRateLimitingWrapper(reporters, rateLimitConfig)) 34 | } 35 | 36 | var app pitaya.Pitaya 37 | 38 | func main() { 39 | port := flag.Int("port", 3250, "the port to listen") 40 | svType := "room" 41 | 42 | flag.Parse() 43 | 44 | config := config.NewDefaultPitayaConfig() 45 | builder := pitaya.NewDefaultBuilder(true, svType, pitaya.Cluster, map[string]string{}, *config) 46 | builder.AddAcceptor(createAcceptor(*port, builder.MetricsReporters)) 47 | 48 | app = builder.Build() 49 | 50 | defer app.Shutdown() 51 | 52 | room := services.NewRoom() 53 | app.Register(room, 54 | component.WithName("room"), 55 | component.WithNameFunc(strings.ToLower), 56 | ) 57 | 58 | app.Start() 59 | } 60 | -------------------------------------------------------------------------------- /examples/demo/rate_limiting/services/room.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/topfreegames/pitaya/v3/pkg/component" 7 | ) 8 | 9 | // Room represents a component that contains a bundle of room related handler 10 | type Room struct { 11 | component.Base 12 | } 13 | 14 | // NewRoom returns a new room 15 | func NewRoom() *Room { 16 | return &Room{} 17 | } 18 | 19 | // Ping returns a pong 20 | func (r *Room) Ping(ctx context.Context) ([]byte, error) { 21 | return []byte("pong"), nil 22 | } 23 | -------------------------------------------------------------------------------- /examples/demo/worker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "strings" 8 | 9 | "github.com/spf13/viper" 10 | "github.com/topfreegames/pitaya/v3/examples/demo/worker/services" 11 | pitaya "github.com/topfreegames/pitaya/v3/pkg" 12 | "github.com/topfreegames/pitaya/v3/pkg/acceptor" 13 | "github.com/topfreegames/pitaya/v3/pkg/component" 14 | "github.com/topfreegames/pitaya/v3/pkg/config" 15 | ) 16 | 17 | var app pitaya.Pitaya 18 | 19 | func configureWorker() { 20 | worker := services.Worker{} 21 | worker.Configure(app) 22 | } 23 | 24 | func main() { 25 | port := flag.Int("port", 3250, "the port to listen") 26 | svType := flag.String("type", "metagame", "the server type") 27 | isFrontend := flag.Bool("frontend", true, "if server is frontend") 28 | 29 | flag.Parse() 30 | 31 | conf := viper.New() 32 | conf.SetDefault("pitaya.worker.redis.url", "localhost:6379") 33 | conf.SetDefault("pitaya.worker.redis.pool", "3") 34 | 35 | config := config.NewConfig(conf) 36 | 37 | tcp := acceptor.NewTCPAcceptor(fmt.Sprintf(":%d", *port)) 38 | 39 | builder := pitaya.NewBuilderWithConfigs(*isFrontend, *svType, pitaya.Cluster, map[string]string{}, config) 40 | if *isFrontend { 41 | builder.AddAcceptor(tcp) 42 | } 43 | app = builder.Build() 44 | 45 | defer app.Shutdown() 46 | 47 | switch *svType { 48 | case "metagame": 49 | app.RegisterRemote(&services.Metagame{}, 50 | component.WithName("metagame"), 51 | component.WithNameFunc(strings.ToLower), 52 | ) 53 | case "room": 54 | app.Register(services.NewRoom(app), 55 | component.WithName("room"), 56 | component.WithNameFunc(strings.ToLower), 57 | ) 58 | case "worker": 59 | configureWorker() 60 | } 61 | 62 | app.Start() 63 | } 64 | -------------------------------------------------------------------------------- /examples/demo/worker/protos/arg.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: arg.proto 3 | 4 | package protos 5 | 6 | import proto "github.com/golang/protobuf/proto" 7 | import fmt "fmt" 8 | import math "math" 9 | 10 | // Reference imports to suppress errors if they are not otherwise used. 11 | var _ = proto.Marshal 12 | var _ = fmt.Errorf 13 | var _ = math.Inf 14 | 15 | // This is a compile-time assertion to ensure that this generated file 16 | // is compatible with the proto package it is being compiled against. 17 | // A compilation error at this line likely means your copy of the 18 | // proto package needs to be updated. 19 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 20 | 21 | type Arg struct { 22 | Msg string `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` 23 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 24 | XXX_unrecognized []byte `json:"-"` 25 | XXX_sizecache int32 `json:"-"` 26 | } 27 | 28 | func (m *Arg) Reset() { *m = Arg{} } 29 | func (m *Arg) String() string { return proto.CompactTextString(m) } 30 | func (*Arg) ProtoMessage() {} 31 | func (*Arg) Descriptor() ([]byte, []int) { 32 | return fileDescriptor_arg_e1b1c8888ddf9d6e, []int{0} 33 | } 34 | func (m *Arg) XXX_Unmarshal(b []byte) error { 35 | return xxx_messageInfo_Arg.Unmarshal(m, b) 36 | } 37 | func (m *Arg) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 38 | return xxx_messageInfo_Arg.Marshal(b, m, deterministic) 39 | } 40 | func (dst *Arg) XXX_Merge(src proto.Message) { 41 | xxx_messageInfo_Arg.Merge(dst, src) 42 | } 43 | func (m *Arg) XXX_Size() int { 44 | return xxx_messageInfo_Arg.Size(m) 45 | } 46 | func (m *Arg) XXX_DiscardUnknown() { 47 | xxx_messageInfo_Arg.DiscardUnknown(m) 48 | } 49 | 50 | var xxx_messageInfo_Arg proto.InternalMessageInfo 51 | 52 | func (m *Arg) GetMsg() string { 53 | if m != nil { 54 | return m.Msg 55 | } 56 | return "" 57 | } 58 | 59 | func init() { 60 | proto.RegisterType((*Arg)(nil), "protos.Arg") 61 | } 62 | 63 | func init() { proto.RegisterFile("arg.proto", fileDescriptor_arg_e1b1c8888ddf9d6e) } 64 | 65 | var fileDescriptor_arg_e1b1c8888ddf9d6e = []byte{ 66 | // 66 bytes of a gzipped FileDescriptorProto 67 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4c, 0x2c, 0x4a, 0xd7, 68 | 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x03, 0x53, 0xc5, 0x4a, 0xe2, 0x5c, 0xcc, 0x8e, 0x45, 69 | 0xe9, 0x42, 0x02, 0x5c, 0xcc, 0xb9, 0xc5, 0xe9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x20, 70 | 0x66, 0x12, 0x44, 0x81, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0x57, 0x6c, 0xa7, 0xb1, 0x34, 0x00, 71 | 0x00, 0x00, 72 | } 73 | -------------------------------------------------------------------------------- /examples/demo/worker/protos/arg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protos; 4 | 5 | message Arg { 6 | string msg = 1; 7 | } 8 | -------------------------------------------------------------------------------- /examples/demo/worker/protos/response.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protos; 4 | 5 | message Response { 6 | int32 code = 1; 7 | string msg = 2; 8 | } 9 | -------------------------------------------------------------------------------- /examples/demo/worker/services/metagame.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/topfreegames/pitaya/v3/examples/demo/worker/protos" 7 | "github.com/topfreegames/pitaya/v3/pkg/component" 8 | "github.com/topfreegames/pitaya/v3/pkg/logger" 9 | ) 10 | 11 | // Metagame server 12 | type Metagame struct { 13 | component.Base 14 | } 15 | 16 | // LogRemote logs argument when called 17 | func (m *Metagame) LogRemote(ctx context.Context, arg *protos.Arg) (*protos.Response, error) { 18 | logger.Log.Infof("argument %+v\n", arg) 19 | return &protos.Response{Code: 200, Msg: "ok"}, nil 20 | } 21 | -------------------------------------------------------------------------------- /examples/demo/worker/services/room.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/topfreegames/pitaya/v3/examples/demo/worker/protos" 7 | pitaya "github.com/topfreegames/pitaya/v3/pkg" 8 | "github.com/topfreegames/pitaya/v3/pkg/component" 9 | "github.com/topfreegames/pitaya/v3/pkg/logger" 10 | ) 11 | 12 | // Room server 13 | type Room struct { 14 | component.Base 15 | app pitaya.Pitaya 16 | } 17 | 18 | // NewRoom ctor 19 | func NewRoom(app pitaya.Pitaya) *Room { 20 | return &Room{app: app} 21 | } 22 | 23 | // CallLog makes ReliableRPC to metagame LogRemote 24 | func (r *Room) CallLog(ctx context.Context, arg *protos.Arg) (*protos.Response, error) { 25 | route := "metagame.metagame.logremote" 26 | reply := &protos.Response{} 27 | jid, err := r.app.ReliableRPC(route, nil, reply, arg) 28 | if err != nil { 29 | logger.Log.Infof("failed to enqueue rpc: %q", err) 30 | return nil, err 31 | } 32 | 33 | logger.Log.Infof("enqueue rpc job: %d", jid) 34 | return &protos.Response{Code: 200, Msg: "ok"}, nil 35 | } 36 | -------------------------------------------------------------------------------- /examples/demo/worker/services/worker.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/golang/protobuf/proto" 7 | "github.com/topfreegames/pitaya/v3/examples/demo/worker/protos" 8 | pitaya "github.com/topfreegames/pitaya/v3/pkg" 9 | "github.com/topfreegames/pitaya/v3/pkg/component" 10 | ) 11 | 12 | // Worker server 13 | type Worker struct { 14 | component.Base 15 | } 16 | 17 | // Configure starts workers and register rpc job 18 | func (w *Worker) Configure(app pitaya.Pitaya) { 19 | app.StartWorker() 20 | app.RegisterRPCJob(&RPCJob{app: app}) 21 | } 22 | 23 | // RPCJob implements worker.RPCJob 24 | type RPCJob struct { 25 | app pitaya.Pitaya 26 | } 27 | 28 | // ServerDiscovery returns a serverID="", meaning any server 29 | // is ok 30 | func (r *RPCJob) ServerDiscovery( 31 | route string, 32 | rpcMetadata map[string]interface{}, 33 | ) (serverID string, err error) { 34 | return "", nil 35 | } 36 | 37 | // RPC calls pitaya's rpc 38 | func (r *RPCJob) RPC( 39 | ctx context.Context, 40 | serverID, routeStr string, 41 | reply, arg proto.Message, 42 | ) error { 43 | return r.app.RPCTo(ctx, serverID, routeStr, reply, arg) 44 | } 45 | 46 | // GetArgReply returns reply and arg of LogRemote, 47 | // since we have no other methods in this example 48 | func (r *RPCJob) GetArgReply( 49 | route string, 50 | ) (arg, reply proto.Message, err error) { 51 | return &protos.Arg{}, &protos.Response{}, nil 52 | } 53 | -------------------------------------------------------------------------------- /examples/testing/docker-compose-jaeger.yml: -------------------------------------------------------------------------------- 1 | services: 2 | jaeger: 3 | image: jaegertracing/all-in-one:latest 4 | ports: 5 | - "16686:16686" 6 | - "14268:14268" 7 | - "6831:6831/udp" 8 | - "6832:6832/udp" 9 | - "4317:4317" 10 | - "4318:4318" 11 | environment: 12 | - COLLECTOR_OTLP_ENABLED=true 13 | - COLLECTOR_OTLP_HTTP_HOST_PORT=0.0.0.0:4318 14 | - COLLECTOR_OTLP_GRPC_HOST_PORT=0.0.0.0:4317 15 | - LOG_LEVEL=debug 16 | -------------------------------------------------------------------------------- /examples/testing/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | nats: 3 | image: nats 4 | ports: 5 | - 4222:4222 6 | etcd: 7 | image: bitnami/etcd 8 | environment: 9 | - ALLOW_NONE_AUTHENTICATION=yes 10 | ports: 11 | - 2379:2379 12 | redis: 13 | image: redis 14 | ports: 15 | - 6379:6379 16 | -------------------------------------------------------------------------------- /examples/testing/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.7 2 | 3 | ADD ./server /server 4 | 5 | CMD /server 6 | -------------------------------------------------------------------------------- /examples/testing/protos/cluster.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protos; 4 | 5 | // TestRequest message to be sent using rpc 6 | message TestRequest { 7 | string msg = 1; 8 | } 9 | 10 | // TestResponse is the rpc response 11 | message TestResponse { 12 | int32 code = 1; 13 | string msg = 2; 14 | } 15 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.23 2 | 3 | toolchain go1.23.4 4 | 5 | use ( 6 | . 7 | ./xk6-pitaya 8 | ) 9 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Wildlife Studios 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import "github.com/topfreegames/pitaya/v3/cmd" 20 | 21 | func main() { 22 | cmd.Execute() 23 | } 24 | -------------------------------------------------------------------------------- /pkg/acceptor/acceptor.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package acceptor 22 | 23 | import "net" 24 | 25 | // PlayerConn iface 26 | type PlayerConn interface { 27 | GetNextMessage() (b []byte, err error) 28 | RemoteAddr() net.Addr 29 | net.Conn 30 | } 31 | 32 | // Acceptor type interface 33 | type Acceptor interface { 34 | ListenAndServe() 35 | Stop() 36 | GetAddr() string 37 | GetConnChan() chan PlayerConn 38 | EnableProxyProtocol() 39 | IsRunning() bool 40 | GetConfiguredAddress() string 41 | } 42 | -------------------------------------------------------------------------------- /pkg/acceptor/fixtures/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDdDCCAlwCCQDfEyb9MASNvjANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJC 3 | UjELMAkGA1UECAwCU1AxCzAJBgNVBAcMAlNQMQ8wDQYDVQQKDAZQaXRheWExEDAO 4 | BgNVBAsMB0JhY2tlbmQxEjAQBgNVBAMMCWxvY2FsaG9zdDEcMBoGCSqGSIb3DQEJ 5 | ARYNcGl0YXlhQHBpdGF5YTAeFw0xODA0MTAyMjEwMTNaFw0yODA0MDcyMjEwMTNa 6 | MHwxCzAJBgNVBAYTAkJSMQswCQYDVQQIDAJTUDELMAkGA1UEBwwCU1AxDzANBgNV 7 | BAoMBlBpdGF5YTEQMA4GA1UECwwHQmFja2VuZDESMBAGA1UEAwwJbG9jYWxob3N0 8 | MRwwGgYJKoZIhvcNAQkBFg1waXRheWFAcGl0YXlhMIIBIjANBgkqhkiG9w0BAQEF 9 | AAOCAQ8AMIIBCgKCAQEArUveZUOOW2Yl92KpvaAYCwjGy3krUbeSEC0m/FbogWz6 10 | 3PhFNsobKwp4z8UrgJEGQJZTpA4XU4O1MIw9f+u7qbdJxn4sM9aeYyw5ks7VdgdQ 11 | Wy70EJ2qOElhq2I7n0/Dg6cYmOVYif4BHq5mlVYCujKcpsU+bscPaTqxHu+/4QUH 12 | cBAkcm6nC2ucRZsSdTD/M5uxatSjJK6ustgUN3TLMzFSRC6/kvS2/dwIgWeePnID 13 | nFZaAWxq4tlE3oG2aIH7oRMSDpRj29d9DjvoQUPSdnADbkq6+n3GKPPqLo3ArQxx 14 | gvfKKQFgGyTtTUKZQ95BJCFE+SDIxTc/9YxcTsdxDQIDAQABMA0GCSqGSIb3DQEB 15 | CwUAA4IBAQApWiKBLHDrDuwUMS1OESzr1obWesaA3hGXtgEn31+tr0bNeV1F/ZjZ 16 | KhUBGm4kcLERuc7w+J268PDyU3C8tf4ar5NFgAbUgfE2mD6+LGTsSdS06fJVakjl 17 | v1i4mK6eijC3XkWyP8yIVJRSn+uvR7PGXmRFJoQIv96y918UYLgwvh/do4P/JZYy 18 | VNR99XvznijV7Hq0aP4W742PjWjj5/cPFnRJOMNPKFupZvYaKWVv2c1IO4HKkCQ4 19 | FRyO6PIxgNDGOz8Bkc4Z7CMbOyBRpElvOFAE5hsxIufn8DgRkQahfiBOjqpSHq73 20 | eyuaAd8sURZSDx8ToT2loVh+dlSsqtNI 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /pkg/acceptor/fixtures/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCtS95lQ45bZiX3 3 | Yqm9oBgLCMbLeStRt5IQLSb8VuiBbPrc+EU2yhsrCnjPxSuAkQZAllOkDhdTg7Uw 4 | jD1/67upt0nGfiwz1p5jLDmSztV2B1BbLvQQnao4SWGrYjufT8ODpxiY5ViJ/gEe 5 | rmaVVgK6MpymxT5uxw9pOrEe77/hBQdwECRybqcLa5xFmxJ1MP8zm7Fq1KMkrq6y 6 | 2BQ3dMszMVJELr+S9Lb93AiBZ54+cgOcVloBbGri2UTegbZogfuhExIOlGPb130O 7 | O+hBQ9J2cANuSrr6fcYo8+oujcCtDHGC98opAWAbJO1NQplD3kEkIUT5IMjFNz/1 8 | jFxOx3ENAgMBAAECggEBAIglsvObv5vPBMT2nqR7wmfo3UW+TGpG7loHHzngjYoh 9 | NHWmF7qTzgRilcemACdhyKe1csDQ0UKrlw4tH6QAb89GUI/BULjpREZl2FWeadXw 10 | qRD6MoVbWQAfprwe0Pi4kcI85PY6OPUwuXCBU9a/XCUs3iWnkQU3HsRtd/n2IamG 11 | HooKGz5Ij1+FaB2dKUiwYvxsdA9w2Yg54VuuLce/6VgnR79eYEseGvN44Y68JAsU 12 | nwzsXv9eVmn8T22/uWaaDPOaIwysVNI5YI+WyfcI9HQIaJxVAPggoZh4N6pnD6UM 13 | BjI+EJWD3Aiq1Z6CDG4c2FV4Ku0f/l456ykvZrosbAECgYEA3eMTLC0l7H+Msndz 14 | aUzimKqstPKBWqyJ3Z6sXYGO26/z3mZjcl1+A4gZGWegptMk6PsP27jevWRVPuNi 15 | o/eagGcJ3wpNISkwoJgvkhPDmFSFH+d74yCQQRgPsuJbyaHX+RiDTj4sWgBWoq+T 16 | UR9GQxFmGObklMmBF0o/VeU9XjUCgYEAx/BixVC4gvqBKZdlhLF8e6HWFXhNCIsj 17 | 0H6I5vgtSex8I/Zsy320kA0hfzZEuaaGVNrQiYWBAAdAwTKS3W13X8ryBDS6mrCs 18 | BKuM7PDJrS4vKVmiTqMwDpEpL/c9SoOCiPiseZeteoHfKkVsPoQrc2bQzvfttQa6 19 | aHRvyjkTgnkCgYByYbRoeX2rRLVK6rjb9354JMOTI8/65ibL0Bgau8bhCPs2EqIB 20 | OqDTQT1vEzSFyyKj14h9Q/fOugIXwTAARoum1XcJO043YfxnTQx4ySdzR16466O5 21 | mffDFpxBIt8eOggTqMdHdlV2r+X5R3kxwGV//WAcRipfaODbGLM9nEbeYQKBgQCL 22 | JJ8IZLHT7VQARu2OmBpiy/D6RhuOK48EJVtPKj4SaTLHsFJsl5IWghzitDjF3r8z 23 | xIhGfJOXGnUVPwX4dZsTHmCpTqzixLsiEOVla/levXpy039iLK1gJeO9DtonxjgM 24 | 7MrTYByJ2mIdv+yh4Ud/63i74M0cI4+M7CN0X55VOQKBgDw0A5cdenE5VIH2nEsx 25 | SWQ8nSz2qli+59sUcRfDnAWT7iJ5DCzxOVV+wQ/A3D/SbufWJQZpJBSJlXnyY1BX 26 | qFiOrseNsU2R0EDmi8agdgVBqxX/NmF9+Np0G3dFyoeYI68BwpiteCa/Aw2HzPl5 27 | 2SJrVnL52ZOK5WAumUbuWQqA 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /pkg/acceptor/proxyprotowrapper.go: -------------------------------------------------------------------------------- 1 | package acceptor 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | 7 | "github.com/mailgun/proxyproto" 8 | "github.com/topfreegames/pitaya/v3/pkg/logger" 9 | ) 10 | 11 | // Listener is used to wrap an underlying listener, 12 | // whose connections may be using the HAProxy Proxy Protocol. 13 | // If the connection is using the protocol, the RemoteAddr() will return 14 | // the correct client address. 15 | type ProxyProtocolListener struct { 16 | net.Listener 17 | proxyProtocolEnabled *bool 18 | } 19 | 20 | // Accept waits for and returns the next connection to the listener. 21 | func (p *ProxyProtocolListener) Accept() (net.Conn, error) { 22 | // Get the underlying connection 23 | conn, err := p.Listener.Accept() 24 | if err != nil { 25 | return nil, err 26 | } 27 | connP := &Conn{Conn: conn, proxyProtocolEnabled: p.proxyProtocolEnabled} 28 | if *p.proxyProtocolEnabled { 29 | err = connP.checkPrefix() 30 | if err != nil { 31 | return connP, err 32 | } 33 | } 34 | return connP, nil 35 | } 36 | 37 | // Conn is used to wrap and underlying connection which 38 | // may be speaking the Proxy Protocol. If it is, the RemoteAddr() will 39 | // return the address of the client instead of the proxy address. 40 | type Conn struct { 41 | net.Conn 42 | dstAddr *net.Addr 43 | srcAddr *net.Addr 44 | once sync.Once 45 | proxyProtocolEnabled *bool 46 | } 47 | 48 | func (p *Conn) LocalAddr() net.Addr { 49 | if p.dstAddr != nil { 50 | return *p.dstAddr 51 | } 52 | return p.Conn.LocalAddr() 53 | } 54 | 55 | // RemoteAddr returns the address of the client if the proxy 56 | // protocol is being used, otherwise just returns the address of 57 | // the socket peer. If there is an error parsing the header, the 58 | // address of the client is not returned, and the socket is closed. 59 | // Once implication of this is that the call could block if the 60 | // client is slow. Using a Deadline is recommended if this is called 61 | // before Read() 62 | func (p *Conn) RemoteAddr() net.Addr { 63 | if p.srcAddr != nil { 64 | return *p.srcAddr 65 | } 66 | return p.Conn.RemoteAddr() 67 | } 68 | 69 | func (p *Conn) checkPrefix() error { 70 | 71 | h, err := proxyproto.ReadHeader(p) 72 | if err != nil { 73 | logger.Log.Errorf("Failed to read Proxy Protocol TCP header: %s", err.Error()) 74 | p.Close() 75 | return err 76 | 77 | } else if h.Source == nil { 78 | p.Close() 79 | } else { 80 | p.srcAddr = &h.Source 81 | p.dstAddr = &h.Destination 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /pkg/acceptorwrapper/base.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package acceptorwrapper 22 | 23 | import ( 24 | "github.com/topfreegames/pitaya/v3/pkg/acceptor" 25 | ) 26 | 27 | // BaseWrapper implements Wrapper by saving the acceptor as an attribute. 28 | // Conns from acceptor.GetConnChan are processed by wrapConn and 29 | // forwarded to its own connChan. 30 | // Any new wrapper can inherit from BaseWrapper and just implement wrapConn. 31 | type BaseWrapper struct { 32 | acceptor.Acceptor 33 | connChan chan acceptor.PlayerConn 34 | wrapConn func(acceptor.PlayerConn) acceptor.PlayerConn 35 | } 36 | 37 | // NewBaseWrapper returns an instance of BaseWrapper. 38 | func NewBaseWrapper(wrapConn func(acceptor.PlayerConn) acceptor.PlayerConn) BaseWrapper { 39 | return BaseWrapper{ 40 | connChan: make(chan acceptor.PlayerConn), 41 | wrapConn: wrapConn, 42 | } 43 | } 44 | 45 | // ListenAndServe starts a goroutine that wraps acceptor's conn 46 | // and calls acceptor's listenAndServe 47 | func (b *BaseWrapper) ListenAndServe() { 48 | go b.pipe() 49 | b.Acceptor.ListenAndServe() 50 | } 51 | 52 | // GetConnChan returns the wrapper conn chan 53 | func (b *BaseWrapper) GetConnChan() chan acceptor.PlayerConn { 54 | return b.connChan 55 | } 56 | 57 | func (b *BaseWrapper) pipe() { 58 | for conn := range b.Acceptor.GetConnChan() { 59 | b.connChan <- b.wrapConn(conn) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pkg/acceptorwrapper/base_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package acceptorwrapper 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/topfreegames/pitaya/v3/pkg/acceptor" 27 | 28 | "github.com/golang/mock/gomock" 29 | "github.com/stretchr/testify/assert" 30 | "github.com/topfreegames/pitaya/v3/pkg/mocks" 31 | ) 32 | 33 | func TestListenAndServe(t *testing.T) { 34 | t.Parallel() 35 | 36 | ctrl := gomock.NewController(t) 37 | defer ctrl.Finish() 38 | 39 | mockAcceptor := mocks.NewMockAcceptor(ctrl) 40 | mockConn := mocks.NewMockPlayerConn(ctrl) 41 | 42 | conns := make(chan acceptor.PlayerConn) 43 | exit := make(chan struct{}) 44 | reads := 3 45 | go func() { 46 | for i := 0; i < reads; i++ { 47 | mockConn.EXPECT().Read([]byte{}) 48 | conns <- mockConn 49 | } 50 | }() 51 | 52 | mockAcceptor.EXPECT().GetConnChan().Return(conns) 53 | wrapper := &BaseWrapper{ 54 | Acceptor: mockAcceptor, 55 | connChan: make(chan acceptor.PlayerConn), 56 | wrapConn: func(c acceptor.PlayerConn) acceptor.PlayerConn { 57 | _, err := c.Read([]byte{}) 58 | assert.NoError(t, err) 59 | return c 60 | }, 61 | } 62 | 63 | go func() { 64 | i := 0 65 | for range wrapper.GetConnChan() { 66 | i++ 67 | if i == reads { 68 | close(exit) 69 | } 70 | } 71 | }() 72 | 73 | mockAcceptor.EXPECT().ListenAndServe().Do(func() { <-exit }) 74 | wrapper.ListenAndServe() 75 | } 76 | -------------------------------------------------------------------------------- /pkg/acceptorwrapper/rate_limiting_wrapper.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package acceptorwrapper 22 | 23 | import ( 24 | "github.com/topfreegames/pitaya/v3/pkg/acceptor" 25 | "github.com/topfreegames/pitaya/v3/pkg/config" 26 | "github.com/topfreegames/pitaya/v3/pkg/metrics" 27 | ) 28 | 29 | // RateLimitingWrapper rate limits for each connection 30 | // received 31 | type RateLimitingWrapper struct { 32 | BaseWrapper 33 | } 34 | 35 | // NewRateLimitingWrapper returns an instance of *RateLimitingWrapper 36 | func NewRateLimitingWrapper(reporters []metrics.Reporter, c config.RateLimitingConfig) *RateLimitingWrapper { 37 | r := &RateLimitingWrapper{} 38 | 39 | r.BaseWrapper = NewBaseWrapper(func(conn acceptor.PlayerConn) acceptor.PlayerConn { 40 | return NewRateLimiter(reporters, conn, c.Limit, c.Interval, c.ForceDisable) 41 | }) 42 | 43 | return r 44 | } 45 | 46 | // Wrap saves acceptor as an attribute 47 | func (r *RateLimitingWrapper) Wrap(a acceptor.Acceptor) acceptor.Acceptor { 48 | r.Acceptor = a 49 | return r 50 | } 51 | -------------------------------------------------------------------------------- /pkg/acceptorwrapper/rate_limiting_wrapper_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package acceptorwrapper 22 | 23 | import ( 24 | "testing" 25 | "time" 26 | 27 | "github.com/stretchr/testify/assert" 28 | "github.com/topfreegames/pitaya/v3/pkg/config" 29 | "github.com/topfreegames/pitaya/v3/pkg/metrics" 30 | ) 31 | 32 | func TestNewRateLimitingWrapper(t *testing.T) { 33 | t.Parallel() 34 | 35 | reporters := []metrics.Reporter{} 36 | 37 | rateLimitingWrapper := NewRateLimitingWrapper(reporters, config.NewDefaultPitayaConfig().Conn.RateLimiting) 38 | expected := NewRateLimiter(reporters, nil, 20, time.Second, false) 39 | assert.Equal(t, expected, rateLimitingWrapper.wrapConn(nil)) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/acceptorwrapper/wrapper.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package acceptorwrapper 22 | 23 | import ( 24 | "github.com/topfreegames/pitaya/v3/pkg/acceptor" 25 | ) 26 | 27 | // Wrapper has a method that receives an acceptor and the struct 28 | // that implements must encapsulate it. The main goal is to create 29 | // a middleware for packets of net.Conn from acceptor.GetConnChan before 30 | // giving it to serviceHandler. 31 | type Wrapper interface { 32 | Wrap(acceptor.Acceptor) acceptor.Acceptor 33 | } 34 | 35 | // WithWrappers walks through wrappers calling Wrapper 36 | func WithWrappers( 37 | a acceptor.Acceptor, 38 | wrappers ...Wrapper, 39 | ) acceptor.Acceptor { 40 | for _, w := range wrappers { 41 | a = w.Wrap(a) 42 | } 43 | return a 44 | } 45 | -------------------------------------------------------------------------------- /pkg/builder_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) nano Author and TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package pitaya 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | "github.com/topfreegames/pitaya/v3/pkg/acceptor" 28 | "github.com/topfreegames/pitaya/v3/pkg/config" 29 | ) 30 | 31 | func TestPostBuildHooks(t *testing.T) { 32 | acc := acceptor.NewTCPAcceptor("0.0.0.0:0") 33 | for _, table := range tables { 34 | builderConfig := config.NewDefaultPitayaConfig() 35 | 36 | t.Run("with_post_build_hooks", func(t *testing.T) { 37 | called := false 38 | builder := NewDefaultBuilder(table.isFrontend, table.serverType, table.serverMode, table.serverMetadata, *builderConfig) 39 | builder.AddAcceptor(acc) 40 | builder.AddPostBuildHook(func(app Pitaya) { 41 | called = true 42 | }) 43 | app := builder.Build() 44 | 45 | assert.True(t, called) 46 | assert.NotNil(t, app) 47 | }) 48 | 49 | t.Run("without_post_build_hooks", func(t *testing.T) { 50 | called := false 51 | builder := NewDefaultBuilder(table.isFrontend, table.serverType, table.serverMode, table.serverMetadata, *builderConfig) 52 | builder.AddAcceptor(acc) 53 | app := builder.Build() 54 | 55 | assert.False(t, called) 56 | assert.NotNil(t, app) 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/client/client_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/golang/mock/gomock" 8 | "github.com/sirupsen/logrus" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/topfreegames/pitaya/v3/pkg/conn/message" 11 | "github.com/topfreegames/pitaya/v3/pkg/helpers" 12 | "github.com/topfreegames/pitaya/v3/pkg/mocks" 13 | ) 14 | 15 | func TestSendRequestShouldTimeout(t *testing.T) { 16 | c := New(logrus.InfoLevel, 100*time.Millisecond) 17 | ctrl := gomock.NewController(t) 18 | defer ctrl.Finish() 19 | 20 | mockConn := mocks.NewMockPlayerConn(ctrl) 21 | c.conn = mockConn 22 | go c.pendingRequestsReaper() 23 | 24 | route := "com.sometest.route" 25 | data := []byte{0x02, 0x03, 0x04} 26 | 27 | m := message.Message{ 28 | Type: message.Request, 29 | ID: 1, 30 | Route: route, 31 | Data: data, 32 | Err: false, 33 | } 34 | 35 | pkt, err := c.buildPacket(m) 36 | assert.NoError(t, err) 37 | 38 | mockConn.EXPECT().Write(pkt) 39 | 40 | c.IncomingMsgChan = make(chan *message.Message, 10) 41 | 42 | c.nextID = 0 43 | c.SendRequest(route, data) 44 | 45 | msg := helpers.ShouldEventuallyReceive(t, c.IncomingMsgChan, 2*time.Second).(*message.Message) 46 | 47 | assert.Equal(t, true, msg.Err) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/client/pitayaclient.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package client 22 | 23 | import ( 24 | "crypto/tls" 25 | 26 | "github.com/topfreegames/pitaya/v3/pkg/conn/message" 27 | "github.com/topfreegames/pitaya/v3/pkg/session" 28 | ) 29 | 30 | // PitayaClient iface 31 | type PitayaClient interface { 32 | ConnectTo(addr string, tlsConfig ...*tls.Config) error 33 | ConnectToWS(addr string, path string, tlsConfig ...*tls.Config) error 34 | ConnectedStatus() bool 35 | Disconnect() 36 | MsgChannel() chan *message.Message 37 | SendNotify(route string, data []byte) error 38 | SendRequest(route string, data []byte) (uint, error) 39 | SetClientHandshakeData(data *session.HandshakeData) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/cluster/grpc_rpc_server_test.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "testing" 7 | 8 | "github.com/golang/mock/gomock" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/topfreegames/pitaya/v3/pkg/config" 12 | "github.com/topfreegames/pitaya/v3/pkg/helpers" 13 | "github.com/topfreegames/pitaya/v3/pkg/metrics" 14 | protosmocks "github.com/topfreegames/pitaya/v3/pkg/protos/mocks" 15 | ) 16 | 17 | func TestNewGRPCServer(t *testing.T) { 18 | t.Parallel() 19 | sv := getServer() 20 | gs, err := NewGRPCServer(config.NewDefaultPitayaConfig().Cluster.RPC.Server.Grpc, sv, []metrics.Reporter{}) 21 | assert.NoError(t, err) 22 | assert.NotNil(t, gs) 23 | } 24 | 25 | func TestGRPCServerInit(t *testing.T) { 26 | t.Parallel() 27 | c := config.NewDefaultPitayaConfig().Cluster.RPC.Server.Grpc 28 | c.Port = helpers.GetFreePort(t) 29 | ctrl := gomock.NewController(t) 30 | defer ctrl.Finish() 31 | mockPitayaServer := protosmocks.NewMockPitayaServer(ctrl) 32 | 33 | sv := getServer() 34 | gs, err := NewGRPCServer(c, sv, []metrics.Reporter{}) 35 | gs.SetPitayaServer(mockPitayaServer) 36 | err = gs.Init() 37 | assert.NoError(t, err) 38 | assert.NotNil(t, gs) 39 | 40 | conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", c.Port)) 41 | assert.NoError(t, err) 42 | assert.NotNil(t, conn) 43 | assert.NotNil(t, gs.grpcSv) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/cluster/info_retriever.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import "github.com/topfreegames/pitaya/v3/pkg/config" 4 | 5 | // infoRetriever gets cluster info from config 6 | // Implements InfoRetriever interface 7 | type infoRetriever struct { 8 | region string 9 | } 10 | 11 | // NewInfoRetriever returns a *infoRetriever 12 | func NewInfoRetriever(config config.InfoRetrieverConfig) InfoRetriever { 13 | return &infoRetriever{ 14 | region: config.Region, 15 | } 16 | } 17 | 18 | // Region gets server's region from config 19 | func (c *infoRetriever) Region() string { 20 | return c.region 21 | } 22 | -------------------------------------------------------------------------------- /pkg/cluster/info_retriever_test.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/spf13/viper" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/topfreegames/pitaya/v3/pkg/config" 9 | ) 10 | 11 | func TestInfoRetrieverRegion(t *testing.T) { 12 | t.Parallel() 13 | 14 | c := viper.New() 15 | c.Set("pitaya.cluster.info.region", "us") 16 | conf := config.NewConfig(c) 17 | 18 | infoRetriever := NewInfoRetriever(*&config.NewPitayaConfig(conf).Cluster.Info) 19 | 20 | assert.Equal(t, "us", infoRetriever.Region()) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/cluster/nats_rpc_common.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package cluster 22 | 23 | import ( 24 | "fmt" 25 | 26 | nats "github.com/nats-io/nats.go" 27 | "github.com/topfreegames/pitaya/v3/pkg/logger" 28 | ) 29 | 30 | func getChannel(serverType, serverID string) string { 31 | return fmt.Sprintf("pitaya/servers/%s/%s", serverType, serverID) 32 | } 33 | 34 | func setupNatsConn(connectString string, appDieChan chan bool, options ...nats.Option) (*nats.Conn, error) { 35 | natsOptions := append( 36 | options, 37 | nats.DisconnectErrHandler(func(_ *nats.Conn, err error) { 38 | logger.Log.Warnf("disconnected from nats! Reason: %q\n", err) 39 | }), 40 | nats.ReconnectHandler(func(nc *nats.Conn) { 41 | logger.Log.Warnf("reconnected to nats server %s with address %s in cluster %s!", nc.ConnectedServerName(), nc.ConnectedAddr(), nc.ConnectedClusterName()) 42 | }), 43 | nats.ClosedHandler(func(nc *nats.Conn) { 44 | err := nc.LastError() 45 | if err == nil { 46 | logger.Log.Warn("nats connection closed with no error.") 47 | return 48 | } 49 | 50 | logger.Log.Errorf("nats connection closed. reason: %q", nc.LastError()) 51 | if appDieChan != nil { 52 | appDieChan <- true 53 | } 54 | }), 55 | nats.ErrorHandler(func(nc *nats.Conn, sub *nats.Subscription, err error) { 56 | if err == nats.ErrSlowConsumer { 57 | dropped, _ := sub.Dropped() 58 | logger.Log.Warn("nats slow consumer on subject %q: dropped %d messages\n", 59 | sub.Subject, dropped) 60 | } else { 61 | logger.Log.Errorf(err.Error()) 62 | } 63 | }), 64 | ) 65 | 66 | nc, err := nats.Connect(connectString, natsOptions...) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return nc, nil 71 | } 72 | -------------------------------------------------------------------------------- /pkg/cluster/nats_rpc_common_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package cluster 22 | 23 | import ( 24 | "fmt" 25 | "testing" 26 | "time" 27 | 28 | nats "github.com/nats-io/nats.go" 29 | "github.com/stretchr/testify/assert" 30 | "github.com/topfreegames/pitaya/v3/pkg/helpers" 31 | ) 32 | 33 | func getServer() *Server { 34 | return &Server{ 35 | ID: "id1", 36 | Type: "type1", 37 | Frontend: true, 38 | } 39 | } 40 | 41 | func TestNatsRPCCommonGetChannel(t *testing.T) { 42 | t.Parallel() 43 | assert.Equal(t, "pitaya/servers/type1/sv1", getChannel("type1", "sv1")) 44 | assert.Equal(t, "pitaya/servers/2type1/2sv1", getChannel("2type1", "2sv1")) 45 | } 46 | 47 | func TestNatsRPCCommonSetupNatsConn(t *testing.T) { 48 | t.Parallel() 49 | s := helpers.GetTestNatsServer(t) 50 | defer s.Shutdown() 51 | conn, err := setupNatsConn(fmt.Sprintf("nats://%s", s.Addr()), nil) 52 | assert.NoError(t, err) 53 | assert.NotNil(t, conn) 54 | } 55 | 56 | func TestNatsRPCCommonSetupNatsConnShouldError(t *testing.T) { 57 | t.Parallel() 58 | conn, err := setupNatsConn("nats://localhost:1234", nil) 59 | assert.Error(t, err) 60 | assert.Nil(t, conn) 61 | } 62 | 63 | func TestNatsRPCCommonCloseHandler(t *testing.T) { 64 | t.Parallel() 65 | s := helpers.GetTestNatsServer(t) 66 | 67 | dieChan := make(chan bool) 68 | 69 | conn, err := setupNatsConn(fmt.Sprintf("nats://%s", s.Addr()), dieChan, nats.MaxReconnects(1), 70 | nats.ReconnectWait(1*time.Millisecond)) 71 | assert.NoError(t, err) 72 | assert.NotNil(t, conn) 73 | 74 | s.Shutdown() 75 | 76 | value, ok := <-dieChan 77 | assert.True(t, ok) 78 | assert.True(t, value) 79 | } 80 | -------------------------------------------------------------------------------- /pkg/cluster/server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package cluster 22 | 23 | import ( 24 | "encoding/json" 25 | "os" 26 | 27 | "github.com/topfreegames/pitaya/v3/pkg/logger" 28 | ) 29 | 30 | // Server struct 31 | type Server struct { 32 | ID string `json:"id"` 33 | Type string `json:"type"` 34 | Metadata map[string]string `json:"metadata"` 35 | Frontend bool `json:"frontend"` 36 | Hostname string `json:"hostname"` 37 | } 38 | 39 | // NewServer ctor 40 | func NewServer(id, serverType string, frontend bool, metadata ...map[string]string) *Server { 41 | d := make(map[string]string) 42 | h, err := os.Hostname() 43 | if err != nil { 44 | logger.Log.Errorf("failed to get hostname: %s", err.Error()) 45 | } 46 | if len(metadata) > 0 { 47 | d = metadata[0] 48 | } 49 | return &Server{ 50 | ID: id, 51 | Type: serverType, 52 | Metadata: d, 53 | Frontend: frontend, 54 | Hostname: h, 55 | } 56 | } 57 | 58 | // AsJSONString returns the server as a json string 59 | func (s *Server) AsJSONString() string { 60 | str, err := json.Marshal(s) 61 | if err != nil { 62 | logger.Log.Errorf("error getting server as json: %s", err.Error()) 63 | return "" 64 | } 65 | return string(str) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/cluster/server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package cluster 22 | 23 | import ( 24 | "encoding/json" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | var svTestTables = []struct { 31 | id string 32 | svType string 33 | metadata map[string]string 34 | frontend bool 35 | }{ 36 | {"someid-1", "somesvtype", map[string]string{"bla": "ola"}, true}, 37 | {"someid-2", "somesvtype", nil, true}, 38 | {"someid-3", "somesvtype", map[string]string{"sv": "game"}, false}, 39 | {"someid-4", "somesvtype", map[string]string{}, false}, 40 | } 41 | 42 | func TestNewServer(t *testing.T) { 43 | t.Parallel() 44 | for _, table := range svTestTables { 45 | t.Run(table.id, func(t *testing.T) { 46 | s := NewServer(table.id, table.svType, table.frontend, table.metadata) 47 | assert.Equal(t, table.id, s.ID) 48 | assert.Equal(t, table.metadata, s.Metadata) 49 | assert.Equal(t, table.frontend, s.Frontend) 50 | assert.NotNil(t, s.Hostname) 51 | }) 52 | } 53 | } 54 | 55 | func TestAsJSONString(t *testing.T) { 56 | t.Parallel() 57 | for _, table := range svTestTables { 58 | t.Run(table.id, func(t *testing.T) { 59 | s := NewServer(table.id, table.svType, table.frontend, table.metadata) 60 | b, err := json.Marshal(s) 61 | assert.NoError(t, err) 62 | assert.Equal(t, string(b), s.AsJSONString()) 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pkg/cluster/service_discovery.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package cluster 22 | 23 | import "github.com/topfreegames/pitaya/v3/pkg/interfaces" 24 | 25 | // ServiceDiscovery is the interface for a service discovery client 26 | type ServiceDiscovery interface { 27 | GetServersByType(serverType string) (map[string]*Server, error) 28 | GetServer(id string) (*Server, error) 29 | GetServers() []*Server 30 | SyncServers(firstSync bool) error 31 | AddListener(listener SDListener) 32 | interfaces.Module 33 | } 34 | -------------------------------------------------------------------------------- /pkg/component/base.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) nano Author and TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package component 22 | 23 | // Base implements a default component for Component. 24 | type Base struct{} 25 | 26 | // Init was called to initialize the component. 27 | func (c *Base) Init() {} 28 | 29 | // AfterInit was called after the component is initialized. 30 | func (c *Base) AfterInit() {} 31 | 32 | // BeforeShutdown was called before the component to shutdown. 33 | func (c *Base) BeforeShutdown() {} 34 | 35 | // Shutdown was called to shutdown the component. 36 | func (c *Base) Shutdown() {} 37 | -------------------------------------------------------------------------------- /pkg/component/component.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) nano Author and TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package component 22 | 23 | // Component is the interface that represent a component. 24 | type Component interface { 25 | Init() 26 | AfterInit() 27 | BeforeShutdown() 28 | Shutdown() 29 | } 30 | -------------------------------------------------------------------------------- /pkg/component/options.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) nano Authors and TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package component 22 | 23 | type ( 24 | options struct { 25 | name string // component name 26 | nameFunc func(string) string // rename handler name 27 | } 28 | 29 | // Option used to customize handler 30 | Option func(options *options) 31 | ) 32 | 33 | // WithName used to rename component name 34 | func WithName(name string) Option { 35 | return func(opt *options) { 36 | opt.name = name 37 | } 38 | } 39 | 40 | // WithNameFunc override handler name by specific function 41 | // such as: strings.ToUpper/strings.ToLower 42 | func WithNameFunc(fn func(string) string) Option { 43 | return func(opt *options) { 44 | opt.nameFunc = fn 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pkg/component/options_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package component 22 | 23 | import ( 24 | "strings" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func TestWithName(t *testing.T) { 31 | name := "someName" 32 | opt := &options{} 33 | WithName(name)(opt) 34 | assert.Equal(t, name, opt.name) 35 | } 36 | 37 | func TestWithNameFunc(t *testing.T) { 38 | name := "somename" 39 | opt := &options{} 40 | nameFunc := func(s string) string { 41 | return strings.ToUpper(s) 42 | } 43 | WithNameFunc(nameFunc)(opt) 44 | assert.Equal(t, opt.nameFunc(name), strings.ToUpper(name)) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/conn/codec/constants.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) nano Author and TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package codec 22 | 23 | import "errors" 24 | 25 | // Codec constants. 26 | const ( 27 | HeadLength = 4 28 | MaxPacketSize = 1 << 24 //16MB 29 | ) 30 | 31 | // ErrPacketSizeExcced is the error used for encode/decode. 32 | var ErrPacketSizeExcced = errors.New("codec: packet size exceed") 33 | -------------------------------------------------------------------------------- /pkg/conn/codec/mocks/packet_decoder.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: conn/codec/packet_decoder.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | packet "github.com/topfreegames/pitaya/v3/pkg/conn/packet" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockPacketDecoder is a mock of PacketDecoder interface 14 | type MockPacketDecoder struct { 15 | ctrl *gomock.Controller 16 | recorder *MockPacketDecoderMockRecorder 17 | } 18 | 19 | // MockPacketDecoderMockRecorder is the mock recorder for MockPacketDecoder 20 | type MockPacketDecoderMockRecorder struct { 21 | mock *MockPacketDecoder 22 | } 23 | 24 | // NewMockPacketDecoder creates a new mock instance 25 | func NewMockPacketDecoder(ctrl *gomock.Controller) *MockPacketDecoder { 26 | mock := &MockPacketDecoder{ctrl: ctrl} 27 | mock.recorder = &MockPacketDecoderMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockPacketDecoder) EXPECT() *MockPacketDecoderMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // Decode mocks base method 37 | func (m *MockPacketDecoder) Decode(data []byte) ([]*packet.Packet, error) { 38 | ret := m.ctrl.Call(m, "Decode", data) 39 | ret0, _ := ret[0].([]*packet.Packet) 40 | ret1, _ := ret[1].(error) 41 | return ret0, ret1 42 | } 43 | 44 | // Decode indicates an expected call of Decode 45 | func (mr *MockPacketDecoderMockRecorder) Decode(data interface{}) *gomock.Call { 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Decode", reflect.TypeOf((*MockPacketDecoder)(nil).Decode), data) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/conn/codec/mocks/packet_encoder.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: conn/codec/packet_encoder.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | packet "github.com/topfreegames/pitaya/v3/pkg/conn/packet" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockPacketEncoder is a mock of PacketEncoder interface 14 | type MockPacketEncoder struct { 15 | ctrl *gomock.Controller 16 | recorder *MockPacketEncoderMockRecorder 17 | } 18 | 19 | // MockPacketEncoderMockRecorder is the mock recorder for MockPacketEncoder 20 | type MockPacketEncoderMockRecorder struct { 21 | mock *MockPacketEncoder 22 | } 23 | 24 | // NewMockPacketEncoder creates a new mock instance 25 | func NewMockPacketEncoder(ctrl *gomock.Controller) *MockPacketEncoder { 26 | mock := &MockPacketEncoder{ctrl: ctrl} 27 | mock.recorder = &MockPacketEncoderMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockPacketEncoder) EXPECT() *MockPacketEncoderMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // Encode mocks base method 37 | func (m *MockPacketEncoder) Encode(typ packet.Type, data []byte) ([]byte, error) { 38 | ret := m.ctrl.Call(m, "Encode", typ, data) 39 | ret0, _ := ret[0].([]byte) 40 | ret1, _ := ret[1].(error) 41 | return ret0, ret1 42 | } 43 | 44 | // Encode indicates an expected call of Encode 45 | func (mr *MockPacketEncoderMockRecorder) Encode(typ, data interface{}) *gomock.Call { 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Encode", reflect.TypeOf((*MockPacketEncoder)(nil).Encode), typ, data) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/conn/codec/packet_decoder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package codec 22 | 23 | import "github.com/topfreegames/pitaya/v3/pkg/conn/packet" 24 | 25 | // PacketDecoder interface 26 | type PacketDecoder interface { 27 | Decode(data []byte) ([]*packet.Packet, error) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/conn/codec/packet_encoder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package codec 22 | 23 | import "github.com/topfreegames/pitaya/v3/pkg/conn/packet" 24 | 25 | // PacketEncoder interface 26 | type PacketEncoder interface { 27 | Encode(typ packet.Type, data []byte) ([]byte, error) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/conn/codec/pomelo_packet_decoder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) nano Author and TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package codec 22 | 23 | import ( 24 | "bytes" 25 | 26 | "github.com/topfreegames/pitaya/v3/pkg/conn/packet" 27 | ) 28 | 29 | // PomeloPacketDecoder reads and decodes network data slice following pomelo's protocol 30 | type PomeloPacketDecoder struct{} 31 | 32 | // NewPomeloPacketDecoder returns a new decoder that used for decode network bytes slice. 33 | func NewPomeloPacketDecoder() *PomeloPacketDecoder { 34 | return &PomeloPacketDecoder{} 35 | } 36 | 37 | func (c *PomeloPacketDecoder) forward(buf *bytes.Buffer) (int, packet.Type, error) { 38 | header := buf.Next(HeadLength) 39 | return ParseHeader(header) 40 | } 41 | 42 | // Decode decode the network bytes slice to packet.Packet(s) 43 | func (c *PomeloPacketDecoder) Decode(data []byte) ([]*packet.Packet, error) { 44 | buf := bytes.NewBuffer(nil) 45 | buf.Write(data) 46 | 47 | var ( 48 | packets []*packet.Packet 49 | err error 50 | ) 51 | // check length 52 | if buf.Len() < HeadLength { 53 | return nil, nil 54 | } 55 | 56 | // first time 57 | size, typ, err := c.forward(buf) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | for size <= buf.Len() { 63 | p := &packet.Packet{Type: typ, Length: size, Data: buf.Next(size)} 64 | packets = append(packets, p) 65 | 66 | // if no more packets, break 67 | if buf.Len() < HeadLength { 68 | break 69 | } 70 | 71 | size, typ, err = c.forward(buf) 72 | if err != nil { 73 | return nil, err 74 | } 75 | } 76 | 77 | return packets, nil 78 | } 79 | -------------------------------------------------------------------------------- /pkg/conn/codec/pomelo_packet_decoder_test.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/topfreegames/pitaya/v3/pkg/conn/packet" 9 | ) 10 | 11 | var forwardTables = map[string]struct { 12 | buf []byte 13 | err error 14 | }{ 15 | "test_handshake_type": {[]byte{packet.Handshake, 0x00, 0x00, 0x00}, nil}, 16 | "test_handshake_ack_type": {[]byte{packet.HandshakeAck, 0x00, 0x00, 0x00}, nil}, 17 | "test_heartbeat_type": {[]byte{packet.Heartbeat, 0x00, 0x00, 0x00}, nil}, 18 | "test_data_type": {[]byte{packet.Data, 0x00, 0x00, 0x00}, nil}, 19 | "test_kick_type": {[]byte{packet.Kick, 0x00, 0x00, 0x00}, nil}, 20 | 21 | "test_wrong_packet_type": {[]byte{0x06, 0x00, 0x00, 0x00}, packet.ErrWrongPomeloPacketType}, 22 | } 23 | 24 | var ( 25 | handshakeHeaderPacket = []byte{packet.Handshake, 0x00, 0x00, 0x01, 0x01} 26 | invalidHeader = []byte{0xff, 0x00, 0x00, 0x01} 27 | ) 28 | 29 | var decodeTables = map[string]struct { 30 | data []byte 31 | packet []*packet.Packet 32 | err error 33 | }{ 34 | "test_not_enough_bytes": {[]byte{0x01}, nil, nil}, 35 | "test_error_on_forward": {invalidHeader, nil, packet.ErrWrongPomeloPacketType}, 36 | "test_forward": {handshakeHeaderPacket, []*packet.Packet{{packet.Handshake, 1, []byte{0x01}}}, nil}, 37 | "test_forward_many": {append(handshakeHeaderPacket, handshakeHeaderPacket...), []*packet.Packet{{packet.Handshake, 1, []byte{0x01}}, {packet.Handshake, 1, []byte{0x01}}}, nil}, 38 | } 39 | 40 | func TestNewPomeloPacketDecoder(t *testing.T) { 41 | t.Parallel() 42 | 43 | ppd := NewPomeloPacketDecoder() 44 | 45 | assert.NotNil(t, ppd) 46 | } 47 | 48 | func TestForward(t *testing.T) { 49 | t.Parallel() 50 | 51 | for name, table := range forwardTables { 52 | t.Run(name, func(t *testing.T) { 53 | ppd := NewPomeloPacketDecoder() 54 | 55 | sz, typ, err := ppd.forward(bytes.NewBuffer(table.buf)) 56 | if table.err == nil { 57 | assert.Equal(t, packet.Type(table.buf[0]), typ) 58 | assert.Equal(t, 0, sz) 59 | } 60 | 61 | assert.Equal(t, table.err, err) 62 | }) 63 | } 64 | } 65 | 66 | func TestDecode(t *testing.T) { 67 | t.Parallel() 68 | 69 | for name, table := range decodeTables { 70 | t.Run(name, func(t *testing.T) { 71 | ppd := NewPomeloPacketDecoder() 72 | 73 | packet, err := ppd.Decode(table.data) 74 | 75 | assert.Equal(t, table.err, err) 76 | assert.ElementsMatch(t, table.packet, packet) 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pkg/conn/codec/pomelo_packet_encoder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) nano Author and TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package codec 22 | 23 | import ( 24 | "github.com/topfreegames/pitaya/v3/pkg/conn/packet" 25 | ) 26 | 27 | // PomeloPacketEncoder struct 28 | type PomeloPacketEncoder struct { 29 | } 30 | 31 | // NewPomeloPacketEncoder ctor 32 | func NewPomeloPacketEncoder() *PomeloPacketEncoder { 33 | return &PomeloPacketEncoder{} 34 | } 35 | 36 | // Encode create a packet.Packet from the raw bytes slice and then encode to network bytes slice 37 | // Protocol refs: https://github.com/NetEase/pomelo/wiki/Communication-Protocol 38 | // 39 | // --|----------------|-- 40 | // --------|------------------------|-------- 41 | // 1 byte packet type, 3 bytes packet data length(big end), and data segment 42 | func (e *PomeloPacketEncoder) Encode(typ packet.Type, data []byte) ([]byte, error) { 43 | if typ < packet.Handshake || typ > packet.Kick { 44 | return nil, packet.ErrWrongPomeloPacketType 45 | } 46 | 47 | if len(data) > MaxPacketSize { 48 | return nil, ErrPacketSizeExcced 49 | } 50 | 51 | p := &packet.Packet{Type: typ, Length: len(data)} 52 | buf := make([]byte, p.Length+HeadLength) 53 | buf[0] = byte(p.Type) 54 | 55 | copy(buf[1:HeadLength], IntToBytes(p.Length)) 56 | copy(buf[HeadLength:], data) 57 | 58 | return buf, nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/conn/codec/pomelo_packet_encoder_test.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/topfreegames/pitaya/v3/pkg/conn/packet" 8 | ) 9 | 10 | func helperConcatBytes(packetType packet.Type, length, data []byte) []byte { 11 | if data == nil { 12 | return nil 13 | } 14 | 15 | bytes := []byte{} 16 | bytes = append(bytes, byte(packetType)) 17 | bytes = append(bytes, length...) 18 | bytes = append(bytes, data...) 19 | return bytes 20 | } 21 | 22 | var tooBigData = make([]byte, 1<<25) 23 | 24 | var encodeTables = map[string]struct { 25 | packetType packet.Type 26 | length []byte 27 | data []byte 28 | err error 29 | }{ 30 | "test_encode_handshake": {packet.Handshake, []byte{0x00, 0x00, 0x02}, []byte{0x01, 0x00}, nil}, 31 | "test_invalid_packet_type": {0xff, nil, nil, packet.ErrWrongPomeloPacketType}, 32 | "test_too_big_packet": {packet.Data, nil, tooBigData, ErrPacketSizeExcced}, 33 | } 34 | 35 | func TestEncode(t *testing.T) { 36 | t.Parallel() 37 | 38 | for name, table := range encodeTables { 39 | t.Run(name, func(t *testing.T) { 40 | ppe := NewPomeloPacketEncoder() 41 | 42 | encoded, err := ppe.Encode(table.packetType, table.data) 43 | if table.err != nil { 44 | assert.Equal(t, table.err, err) 45 | } else { 46 | expectedEncoded := helperConcatBytes(table.packetType, table.length, table.data) 47 | assert.Equal(t, expectedEncoded, encoded) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/conn/codec/utils.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | import "github.com/topfreegames/pitaya/v3/pkg/conn/packet" 4 | 5 | // ParseHeader parses a packet header and returns its dataLen and packetType or an error 6 | func ParseHeader(header []byte) (int, packet.Type, error) { 7 | if len(header) != HeadLength { 8 | return 0, 0x00, packet.ErrInvalidPomeloHeader 9 | } 10 | typ := header[0] 11 | if typ < packet.Handshake || typ > packet.Kick { 12 | return 0, 0x00, packet.ErrWrongPomeloPacketType 13 | } 14 | 15 | size := BytesToInt(header[1:]) 16 | 17 | if size > MaxPacketSize { 18 | return 0, 0x00, ErrPacketSizeExcced 19 | } 20 | 21 | return size, packet.Type(typ), nil 22 | } 23 | 24 | // BytesToInt decode packet data length byte to int(Big end) 25 | func BytesToInt(b []byte) int { 26 | result := 0 27 | for _, v := range b { 28 | result = result<<8 + int(v) 29 | } 30 | return result 31 | } 32 | 33 | // IntToBytes encode packet data length to bytes(Big end) 34 | func IntToBytes(n int) []byte { 35 | buf := make([]byte, 3) 36 | buf[0] = byte((n >> 16) & 0xFF) 37 | buf[1] = byte((n >> 8) & 0xFF) 38 | buf[2] = byte(n & 0xFF) 39 | return buf 40 | } 41 | -------------------------------------------------------------------------------- /pkg/conn/message/fixtures/test_invalid_message.golden: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfreegames/pitaya/08d34a00bfc535b4e280cf74fd1092562375402d/pkg/conn/message/fixtures/test_invalid_message.golden -------------------------------------------------------------------------------- /pkg/conn/message/fixtures/test_must_gzip.golden: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfreegames/pitaya/08d34a00bfc535b4e280cf74fd1092562375402d/pkg/conn/message/fixtures/test_must_gzip.golden -------------------------------------------------------------------------------- /pkg/conn/message/fixtures/test_notify_type.golden: -------------------------------------------------------------------------------- 1 | a -------------------------------------------------------------------------------- /pkg/conn/message/fixtures/test_notify_type_compressed.golden: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /pkg/conn/message/fixtures/test_push_type.golden: -------------------------------------------------------------------------------- 1 | a -------------------------------------------------------------------------------- /pkg/conn/message/fixtures/test_push_type_compressed.golden: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /pkg/conn/message/fixtures/test_reponse_type.golden: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /pkg/conn/message/fixtures/test_reponse_type_with_data.golden: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /pkg/conn/message/fixtures/test_reponse_type_with_error.golden: -------------------------------------------------------------------------------- 1 | $ -------------------------------------------------------------------------------- /pkg/conn/message/fixtures/test_reponse_type_with_id.golden: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfreegames/pitaya/08d34a00bfc535b4e280cf74fd1092562375402d/pkg/conn/message/fixtures/test_reponse_type_with_id.golden -------------------------------------------------------------------------------- /pkg/conn/message/fixtures/test_request_type.golden: -------------------------------------------------------------------------------- 1 | a -------------------------------------------------------------------------------- /pkg/conn/message/fixtures/test_request_type_compressed.golden: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /pkg/conn/message/fixtures/test_wrong_type.golden: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfreegames/pitaya/08d34a00bfc535b4e280cf74fd1092562375402d/pkg/conn/message/fixtures/test_wrong_type.golden -------------------------------------------------------------------------------- /pkg/conn/message/mocks/message_encoder.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./message_encoder.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | message "github.com/topfreegames/pitaya/v3/pkg/conn/message" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockEncoder is a mock of Encoder interface 14 | type MockEncoder struct { 15 | ctrl *gomock.Controller 16 | recorder *MockEncoderMockRecorder 17 | } 18 | 19 | // MockEncoderMockRecorder is the mock recorder for MockEncoder 20 | type MockEncoderMockRecorder struct { 21 | mock *MockEncoder 22 | } 23 | 24 | // NewMockEncoder creates a new mock instance 25 | func NewMockEncoder(ctrl *gomock.Controller) *MockEncoder { 26 | mock := &MockEncoder{ctrl: ctrl} 27 | mock.recorder = &MockEncoderMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockEncoder) EXPECT() *MockEncoderMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // IsCompressionEnabled mocks base method 37 | func (m *MockEncoder) IsCompressionEnabled() bool { 38 | ret := m.ctrl.Call(m, "IsCompressionEnabled") 39 | ret0, _ := ret[0].(bool) 40 | return ret0 41 | } 42 | 43 | // IsCompressionEnabled indicates an expected call of IsCompressionEnabled 44 | func (mr *MockEncoderMockRecorder) IsCompressionEnabled() *gomock.Call { 45 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCompressionEnabled", reflect.TypeOf((*MockEncoder)(nil).IsCompressionEnabled)) 46 | } 47 | 48 | // Encode mocks base method 49 | func (m *MockEncoder) Encode(message *message.Message) ([]byte, error) { 50 | ret := m.ctrl.Call(m, "Encode", message) 51 | ret0, _ := ret[0].([]byte) 52 | ret1, _ := ret[1].(error) 53 | return ret0, ret1 54 | } 55 | 56 | // Encode indicates an expected call of Encode 57 | func (mr *MockEncoderMockRecorder) Encode(message interface{}) *gomock.Call { 58 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Encode", reflect.TypeOf((*MockEncoder)(nil).Encode), message) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/conn/packet/constants.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) nano Author and TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package packet 22 | 23 | import "errors" 24 | 25 | // Type represents the network packet's type such as: handshake and so on. 26 | type Type byte 27 | 28 | const ( 29 | _ Type = iota 30 | // Handshake represents a handshake: request(client) <====> handshake response(server) 31 | Handshake = 0x01 32 | 33 | // HandshakeAck represents a handshake ack from client to server 34 | HandshakeAck = 0x02 35 | 36 | // Heartbeat represents a heartbeat 37 | Heartbeat = 0x03 38 | 39 | // Data represents a common data packet 40 | Data = 0x04 41 | 42 | // Kick represents a kick off packet 43 | Kick = 0x05 // disconnect message from server 44 | ) 45 | 46 | // ErrWrongPomeloPacketType represents a wrong packet type. 47 | var ErrWrongPomeloPacketType = errors.New("wrong packet type") 48 | 49 | // ErrInvalidPomeloHeader represents an invalid header 50 | var ErrInvalidPomeloHeader = errors.New("invalid header") 51 | -------------------------------------------------------------------------------- /pkg/conn/packet/packet.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) nano Author and TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package packet 22 | 23 | import ( 24 | "fmt" 25 | ) 26 | 27 | // Packet represents a network packet. 28 | type Packet struct { 29 | Type Type 30 | Length int 31 | Data []byte 32 | } 33 | 34 | //New create a Packet instance. 35 | func New() *Packet { 36 | return &Packet{} 37 | } 38 | 39 | //String represents the Packet's in text mode. 40 | func (p *Packet) String() string { 41 | return fmt.Sprintf("Type: %d, Length: %d, Data: %s", p.Type, p.Length, string(p.Data)) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/conn/packet/packet_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package packet 22 | 23 | import ( 24 | "fmt" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func TestNewPacket(t *testing.T) { 31 | p := New() 32 | assert.NotNil(t, p) 33 | } 34 | 35 | func TestString(t *testing.T) { 36 | tables := []struct { 37 | tp Type 38 | data []byte 39 | strOut string 40 | }{ 41 | {Handshake, []byte{0x01}, fmt.Sprintf("Type: %d, Length: %d, Data: %s", Handshake, 1, string([]byte{0x01}))}, 42 | {Data, []byte{0x01, 0x02, 0x03}, fmt.Sprintf("Type: %d, Length: %d, Data: %s", Data, 3, string([]byte{0x01, 0x02, 0x03}))}, 43 | {Kick, []byte{0x05, 0x02, 0x03, 0x04}, fmt.Sprintf("Type: %d, Length: %d, Data: %s", Kick, 4, string([]byte{0x05, 0x02, 0x03, 0x04}))}, 44 | } 45 | 46 | for _, table := range tables { 47 | t.Run(string(table.data), func(t *testing.T) { 48 | p := &Packet{} 49 | p.Data = table.data 50 | p.Type = table.tp 51 | p.Length = len(table.data) 52 | assert.Equal(t, table.strOut, p.String()) 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkg/constants/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) nano Author and TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package constants 22 | 23 | // VERSION returns current pitaya version 24 | var VERSION = "0.1.0" 25 | -------------------------------------------------------------------------------- /pkg/context/fixtures/one_element.golden: -------------------------------------------------------------------------------- 1 | {"key1":"val1"} -------------------------------------------------------------------------------- /pkg/context/fixtures/registered_struct.golden: -------------------------------------------------------------------------------- 1 | {"key1":{}} -------------------------------------------------------------------------------- /pkg/defaultpipelines/default_struct_validator.go: -------------------------------------------------------------------------------- 1 | package defaultpipelines 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | validator "github.com/go-playground/validator/v10" 8 | ) 9 | 10 | // DefaultValidator is the default arguments validator for handlers 11 | // in pitaya 12 | type DefaultValidator struct { 13 | once sync.Once 14 | validate *validator.Validate 15 | } 16 | 17 | // Validate is the the function responsible for validating the 'in' parameter 18 | // based on the struct tags the parameter has. 19 | // This function has the pipeline.Handler signature so 20 | // it is possible to use it as a pipeline function 21 | func (v *DefaultValidator) Validate(ctx context.Context, in interface{}) (context.Context, interface{}, error) { 22 | if in == nil { 23 | return ctx, in, nil 24 | } 25 | 26 | v.lazyinit() 27 | if err := v.validate.Struct(in); err != nil { 28 | return ctx, nil, err 29 | } 30 | 31 | return ctx, in, nil 32 | } 33 | 34 | func (v *DefaultValidator) lazyinit() { 35 | v.once.Do(func() { 36 | v.validate = validator.New() 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/defaultpipelines/default_struct_validator_test.go: -------------------------------------------------------------------------------- 1 | package defaultpipelines 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type TestingStruct struct { 12 | Name string `json:"name"` 13 | Email string `json:"email" validate:"email"` 14 | SoftCurrency int `json:"softCurrency" validate:"gte=0,lte=1000"` 15 | HardCurrency int `json:"hardCurrency" validate:"gte=0,lte=200"` 16 | } 17 | 18 | func TestDefaultValidator(t *testing.T) { 19 | validator := &DefaultValidator{} 20 | 21 | var tableTest = map[string]struct { 22 | s *TestingStruct 23 | shouldFail bool 24 | }{ 25 | "validation_error": { 26 | s: &TestingStruct{ 27 | Email: "notvalid", 28 | }, 29 | shouldFail: true, 30 | }, 31 | "validation_success": { 32 | s: &TestingStruct{ 33 | Name: "foo", 34 | Email: "foo@tfgco.com", 35 | SoftCurrency: 100, 36 | HardCurrency: 10, 37 | }, 38 | }, 39 | "validate_nil_object": { 40 | s: nil, 41 | shouldFail: false, 42 | }, 43 | } 44 | 45 | for tname, tbl := range tableTest { 46 | t.Run(tname, func(t *testing.T) { 47 | var err error 48 | if tbl.s == nil { 49 | _, _, err = validator.Validate(context.Background(), nil) 50 | } else { 51 | _, _, err = validator.Validate(context.Background(), tbl.s) 52 | } 53 | 54 | if tbl.shouldFail { 55 | assert.Error(t, err) 56 | } else { 57 | fmt.Println(err) 58 | assert.NoError(t, err) 59 | } 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/defaultpipelines/struct_validator.go: -------------------------------------------------------------------------------- 1 | package defaultpipelines 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // StructValidator is the interface that must be implemented 8 | // by a struct validator for the request arguments on pitaya. 9 | // 10 | // The default struct validator used by pitaya is https://github.com/go-playground/validator. 11 | type StructValidator interface { 12 | Validate(context.Context, interface{}) (context.Context, interface{}, error) 13 | } 14 | 15 | // StructValidatorInstance holds the default validator 16 | // on start but can be overridden if needed. 17 | var StructValidatorInstance StructValidator = &DefaultValidator{} 18 | -------------------------------------------------------------------------------- /pkg/docgenerator/descriptors.go: -------------------------------------------------------------------------------- 1 | package docgenerator 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "github.com/golang/protobuf/proto" 8 | "github.com/topfreegames/pitaya/v3/pkg/constants" 9 | ) 10 | 11 | // ProtoDescriptors returns the descriptor for a given message name or .proto file 12 | func ProtoDescriptors(protoName string) ([]byte, error) { 13 | if strings.HasSuffix(protoName, ".proto") { 14 | descriptor := proto.FileDescriptor(protoName) 15 | if descriptor == nil { 16 | return nil, constants.ErrProtodescriptor 17 | } 18 | return descriptor, nil 19 | } 20 | 21 | if strings.HasPrefix(protoName, "types.") { 22 | protoName = strings.Replace(protoName, "types.", "google.protobuf.", 1) 23 | } 24 | protoReflectTypePointer := proto.MessageType(protoName) 25 | if protoReflectTypePointer == nil { 26 | return nil, constants.ErrProtodescriptor 27 | } 28 | 29 | protoReflectType := protoReflectTypePointer.Elem() 30 | protoValue := reflect.New(protoReflectType) 31 | descriptorMethod, ok := protoReflectTypePointer.MethodByName("Descriptor") 32 | if !ok { 33 | return nil, constants.ErrProtodescriptor 34 | } 35 | 36 | descriptorValue := descriptorMethod.Func.Call([]reflect.Value{protoValue}) 37 | protoDescriptor := descriptorValue[0].Bytes() 38 | 39 | return protoDescriptor, nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/docgenerator/descriptors_test.go: -------------------------------------------------------------------------------- 1 | package docgenerator 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/topfreegames/pitaya/v3/pkg/constants" 8 | _ "github.com/topfreegames/pitaya/v3/pkg/protos" 9 | ) 10 | 11 | func TestProtoDescriptors(t *testing.T) { 12 | t.Parallel() 13 | tables := []struct { 14 | name string 15 | messageName string 16 | err error 17 | }{ 18 | {"fail filename", "not_exists.proto", constants.ErrProtodescriptor}, 19 | {"success filename", "kick.proto", nil}, 20 | {"success message", "protos.Push", nil}, 21 | {"fail message", "protos.DoNotExist", constants.ErrProtodescriptor}, 22 | } 23 | for _, table := range tables { 24 | t.Run(table.name, func(t *testing.T) { 25 | bts, err := ProtoDescriptors(table.messageName) 26 | if table.err != nil { 27 | assert.EqualError(t, table.err, err.Error()) 28 | assert.Nil(t, bts) 29 | } else { 30 | assert.NoError(t, err) 31 | assert.NotNil(t, bts) 32 | } 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/groups/group_service.go: -------------------------------------------------------------------------------- 1 | package groups 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type ( 9 | // GroupService has ranking methods 10 | GroupService interface { 11 | GroupAddMember(ctx context.Context, groupName, uid string) error 12 | GroupContainsMember(ctx context.Context, groupName, uid string) (bool, error) 13 | GroupCountMembers(ctx context.Context, groupName string) (int, error) 14 | GroupCreate(ctx context.Context, groupName string) error 15 | GroupCreateWithTTL(ctx context.Context, groupName string, ttlTime time.Duration) error 16 | GroupDelete(ctx context.Context, groupName string) error 17 | GroupMembers(ctx context.Context, groupName string) ([]string, error) 18 | GroupRemoveAll(ctx context.Context, groupName string) error 19 | GroupRemoveMember(ctx context.Context, groupName, uid string) error 20 | GroupRenewTTL(ctx context.Context, groupName string) error 21 | Close() 22 | } 23 | ) 24 | 25 | func elementIndex(slice []string, element string) (int, bool) { 26 | for i, sliceElement := range slice { 27 | if element == sliceElement { 28 | return i, true 29 | } 30 | } 31 | return 0, false 32 | } 33 | -------------------------------------------------------------------------------- /pkg/groups/memory_group_service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package groups 22 | 23 | import ( 24 | "os" 25 | "testing" 26 | "time" 27 | 28 | "github.com/topfreegames/pitaya/v3/pkg/config" 29 | ) 30 | 31 | var memoryGroupService *MemoryGroupService 32 | 33 | func TestMain(m *testing.M) { 34 | mconfig := *&config.NewDefaultPitayaConfig().Groups.Memory 35 | mconfig.TickDuration = 10 * time.Millisecond 36 | memoryGroupService = NewMemoryGroupService(mconfig) 37 | exit := m.Run() 38 | time.Sleep(mconfig.TickDuration) 39 | os.Exit(exit) 40 | } 41 | 42 | func TestMemoryCreateDuplicatedGroup(t *testing.T) { 43 | testCreateDuplicatedGroup(memoryGroupService, t) 44 | } 45 | 46 | func TestMemoryCreateGroup(t *testing.T) { 47 | testCreateGroup(memoryGroupService, t) 48 | } 49 | 50 | func TestMemoryCreateGroupWithTTL(t *testing.T) { 51 | testCreateGroupWithTTL(memoryGroupService, t) 52 | } 53 | 54 | func TestMemoryGroupAddMember(t *testing.T) { 55 | testGroupAddMember(memoryGroupService, t) 56 | } 57 | 58 | func TestMemoryGroupAddDuplicatedMember(t *testing.T) { 59 | testGroupAddDuplicatedMember(memoryGroupService, t) 60 | } 61 | 62 | func TestMemoryGroupContainsMember(t *testing.T) { 63 | testGroupContainsMember(memoryGroupService, t) 64 | } 65 | 66 | func TestMemoryRemove(t *testing.T) { 67 | testRemove(memoryGroupService, t) 68 | } 69 | 70 | func TestMemoryDelete(t *testing.T) { 71 | testDelete(memoryGroupService, t) 72 | } 73 | 74 | func TestMemoryRemoveAll(t *testing.T) { 75 | testRemoveAll(memoryGroupService, t) 76 | } 77 | 78 | func TestMemoryCount(t *testing.T) { 79 | testCount(memoryGroupService, t) 80 | } 81 | 82 | func TestMemoryMembers(t *testing.T) { 83 | testMembers(memoryGroupService, t) 84 | } 85 | -------------------------------------------------------------------------------- /pkg/helpers/type_support.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import "reflect" 4 | 5 | func isFunction(f interface{}) bool { 6 | actual := reflect.TypeOf(f) 7 | return actual.Kind() == reflect.Func && actual.NumIn() == 0 && actual.NumOut() > 0 8 | } 9 | 10 | func isChan(a interface{}) bool { 11 | if isNil(a) { 12 | return false 13 | } 14 | return reflect.TypeOf(a).Kind() == reflect.Chan 15 | } 16 | 17 | func isNil(a interface{}) bool { 18 | if a == nil { 19 | return true 20 | } 21 | 22 | switch reflect.TypeOf(a).Kind() { 23 | case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: 24 | return reflect.ValueOf(a).IsNil() 25 | } 26 | 27 | return false 28 | } 29 | -------------------------------------------------------------------------------- /pkg/interfaces/interfaces.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package interfaces 22 | 23 | // Module is the interface that represent a module. 24 | type Module interface { 25 | Init() error 26 | AfterInit() 27 | BeforeShutdown() 28 | Shutdown() error 29 | } 30 | 31 | type SessionModule interface { 32 | Module 33 | StartSessionDraining() 34 | SessionCount() int64 35 | } 36 | 37 | // BindingStorage interface 38 | type BindingStorage interface { 39 | GetUserFrontendID(uid, frontendType string) (string, error) 40 | PutBinding(uid string) error 41 | } 42 | -------------------------------------------------------------------------------- /pkg/kick.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package pitaya 22 | 23 | import ( 24 | "context" 25 | 26 | "github.com/topfreegames/pitaya/v3/pkg/constants" 27 | "github.com/topfreegames/pitaya/v3/pkg/logger" 28 | "github.com/topfreegames/pitaya/v3/pkg/protos" 29 | ) 30 | 31 | // SendKickToUsers sends kick to an user array 32 | func (app *App) SendKickToUsers(uids []string, frontendType string) ([]string, error) { 33 | if !app.server.Frontend && frontendType == "" { 34 | return uids, constants.ErrFrontendTypeNotSpecified 35 | } 36 | 37 | var notKickedUids []string 38 | 39 | for _, uid := range uids { 40 | if s := app.sessionPool.GetSessionByUID(uid); s != nil { 41 | if err := s.Kick(context.Background()); err != nil { 42 | notKickedUids = append(notKickedUids, uid) 43 | logger.Log.Errorf("Session kick error, ID=%d, UID=%s, ERROR=%s", s.ID(), s.UID(), err.Error()) 44 | } 45 | } else if app.rpcClient != nil { 46 | kick := &protos.KickMsg{UserId: uid} 47 | if err := app.rpcClient.SendKick(uid, frontendType, kick); err != nil { 48 | notKickedUids = append(notKickedUids, uid) 49 | logger.Log.Errorf("RPCClient send kick error, UID=%s, SvType=%s, Error=%s", uid, frontendType, err.Error()) 50 | } 51 | } else { 52 | notKickedUids = append(notKickedUids, uid) 53 | } 54 | 55 | } 56 | 57 | if len(notKickedUids) != 0 { 58 | return notKickedUids, constants.ErrKickingUsers 59 | } 60 | 61 | return nil, nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/logger/interfaces/interfaces.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | // Logger interface for pitaya loggers 4 | type Logger interface { 5 | Fatal(format ...interface{}) 6 | Fatalf(format string, args ...interface{}) 7 | Fatalln(args ...interface{}) 8 | 9 | Debug(args ...interface{}) 10 | Debugf(format string, args ...interface{}) 11 | Debugln(args ...interface{}) 12 | 13 | Error(args ...interface{}) 14 | Errorf(format string, args ...interface{}) 15 | Errorln(args ...interface{}) 16 | 17 | Info(args ...interface{}) 18 | Infof(format string, args ...interface{}) 19 | Infoln(args ...interface{}) 20 | 21 | Warn(args ...interface{}) 22 | Warnf(format string, args ...interface{}) 23 | Warnln(args ...interface{}) 24 | 25 | Panic(args ...interface{}) 26 | Panicf(format string, args ...interface{}) 27 | Panicln(args ...interface{}) 28 | 29 | WithFields(fields map[string]interface{}) Logger 30 | WithField(key string, value interface{}) Logger 31 | WithError(err error) Logger 32 | 33 | GetInternalLogger() any 34 | } 35 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package logger 22 | 23 | import ( 24 | "github.com/sirupsen/logrus" 25 | "github.com/topfreegames/pitaya/v3/pkg/logger/interfaces" 26 | logruswrapper "github.com/topfreegames/pitaya/v3/pkg/logger/logrus" 27 | ) 28 | 29 | // Log is the default logger 30 | var Log = initLogger() 31 | 32 | func initLogger() interfaces.Logger { 33 | plog := logrus.New() 34 | plog.Formatter = new(logrus.TextFormatter) 35 | plog.Level = logrus.DebugLevel 36 | 37 | log := plog.WithFields(logrus.Fields{ 38 | "source": "pitaya", 39 | }) 40 | return logruswrapper.NewWithFieldLogger(log) 41 | } 42 | 43 | // SetLogger rewrites the default logger 44 | func SetLogger(l interfaces.Logger) { 45 | if l != nil { 46 | Log = l 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/logger/logger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package logger 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | logruswrapper "github.com/topfreegames/pitaya/v3/pkg/logger/logrus" 28 | ) 29 | 30 | func TestInitLogger(t *testing.T) { 31 | initLogger() 32 | assert.NotNil(t, Log) 33 | } 34 | 35 | func TestSetLogger(t *testing.T) { 36 | l := logruswrapper.New() 37 | SetLogger(l) 38 | assert.Equal(t, l, Log) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/logger/logrus/logrus.go: -------------------------------------------------------------------------------- 1 | package logrus 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "github.com/topfreegames/pitaya/v3/pkg/logger/interfaces" 6 | ) 7 | 8 | type logrusImpl struct { 9 | logrus.FieldLogger 10 | } 11 | 12 | // New returns a new interfaces.Logger implementation based on logrus 13 | func New() interfaces.Logger { 14 | log := logrus.New() 15 | return NewWithLogger(log) 16 | } 17 | 18 | // NewWithEntry returns a new interfaces.Logger implementation based on a provided logrus entry instance 19 | // Deprecated: NewWithEntry is deprecated. 20 | func NewWithEntry(logger *logrus.Entry) interfaces.Logger { 21 | return &logrusImpl{FieldLogger: logger} 22 | } 23 | 24 | // NewWithLogger returns a new interfaces.Logger implementation based on a provided logrus instance 25 | // Deprecated: NewWithLogger is deprecated. 26 | func NewWithLogger(logger *logrus.Logger) interfaces.Logger { 27 | return &logrusImpl{FieldLogger: logrus.NewEntry(logger)} 28 | } 29 | 30 | // NewWithFieldLogger returns a new interfaces.Logger implementation based on a provided logrus instance 31 | func NewWithFieldLogger(logger logrus.FieldLogger) interfaces.Logger { 32 | return &logrusImpl{FieldLogger: logger} 33 | } 34 | 35 | func (l *logrusImpl) WithFields(fields map[string]interface{}) interfaces.Logger { 36 | return &logrusImpl{FieldLogger: l.FieldLogger.WithFields(fields)} 37 | } 38 | 39 | func (l *logrusImpl) WithField(key string, value interface{}) interfaces.Logger { 40 | return &logrusImpl{FieldLogger: l.FieldLogger.WithField(key, value)} 41 | } 42 | 43 | func (l *logrusImpl) WithError(err error) interfaces.Logger { 44 | return &logrusImpl{FieldLogger: l.FieldLogger.WithError(err)} 45 | } 46 | 47 | func (l *logrusImpl) GetInternalLogger() any { 48 | return l.FieldLogger 49 | } 50 | -------------------------------------------------------------------------------- /pkg/logger/test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | tests "github.com/sirupsen/logrus/hooks/test" 5 | "github.com/topfreegames/pitaya/v3/pkg/logger/interfaces" 6 | lwrapper "github.com/topfreegames/pitaya/v3/pkg/logger/logrus" 7 | "io" 8 | ) 9 | 10 | // NewNullLogger creates a discarding logger and installs the test hook. 11 | func NewNullLogger() (interfaces.Logger, *tests.Hook) { 12 | logger, hook := tests.NewNullLogger() 13 | logger.Out = io.Discard 14 | return lwrapper.NewWithFieldLogger(logger), hook 15 | } 16 | -------------------------------------------------------------------------------- /pkg/metrics/constants.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | var ( 4 | // ResponseTime reports the response time of handlers and rpc 5 | ResponseTime = "response_time_ns" 6 | // ConnectedClients represents the number of current connected clients in frontend servers 7 | ConnectedClients = "connected_clients" 8 | // CountServers counts the number of servers of different types 9 | CountServers = "count_servers" 10 | // ChannelCapacity represents the capacity of a channel as a histogram (distribution of available slots) 11 | ChannelCapacity = "channel_capacity" 12 | // DroppedMessages reports the number of dropped messages in rpc server (messages that will not be handled) 13 | DroppedMessages = "dropped_messages" 14 | // ProcessDelay reports the message processing delay to handle the messages at the handler service 15 | ProcessDelay = "handler_delay_ns" 16 | // Goroutines reports the number of goroutines 17 | Goroutines = "goroutines" 18 | // HeapSize reports the size of heap 19 | HeapSize = "heapsize" 20 | // HeapObjects reports the number of allocated heap objects 21 | HeapObjects = "heapobjects" 22 | // WorkerJobsTotal reports the number of executed jobs 23 | WorkerJobsTotal = "worker_jobs_total" 24 | // WorkerJobsRetry reports the number of retried jobs 25 | WorkerJobsRetry = "worker_jobs_retry_total" 26 | // WorkerQueueSize reports the queue size on worker 27 | WorkerQueueSize = "worker_queue_size" 28 | // ExceededRateLimiting reports the number of requests made in a connection 29 | // after the rate limit was exceeded 30 | ExceededRateLimiting = "exceeded_rate_limiting" 31 | ) 32 | -------------------------------------------------------------------------------- /pkg/metrics/mocks/statsd_reporter.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/topfreegames/pitaya/v3/pkg/metrics (interfaces: Client) 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | ) 12 | 13 | // MockClient is a mock of Client interface. 14 | type MockClient struct { 15 | ctrl *gomock.Controller 16 | recorder *MockClientMockRecorder 17 | } 18 | 19 | // MockClientMockRecorder is the mock recorder for MockClient. 20 | type MockClientMockRecorder struct { 21 | mock *MockClient 22 | } 23 | 24 | // NewMockClient creates a new mock instance. 25 | func NewMockClient(ctrl *gomock.Controller) *MockClient { 26 | mock := &MockClient{ctrl: ctrl} 27 | mock.recorder = &MockClientMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use. 32 | func (m *MockClient) EXPECT() *MockClientMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // Count mocks base method. 37 | func (m *MockClient) Count(arg0 string, arg1 int64, arg2 []string, arg3 float64) error { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "Count", arg0, arg1, arg2, arg3) 40 | ret0, _ := ret[0].(error) 41 | return ret0 42 | } 43 | 44 | // Count indicates an expected call of Count. 45 | func (mr *MockClientMockRecorder) Count(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { 46 | mr.mock.ctrl.T.Helper() 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Count", reflect.TypeOf((*MockClient)(nil).Count), arg0, arg1, arg2, arg3) 48 | } 49 | 50 | // Gauge mocks base method. 51 | func (m *MockClient) Gauge(arg0 string, arg1 float64, arg2 []string, arg3 float64) error { 52 | m.ctrl.T.Helper() 53 | ret := m.ctrl.Call(m, "Gauge", arg0, arg1, arg2, arg3) 54 | ret0, _ := ret[0].(error) 55 | return ret0 56 | } 57 | 58 | // Gauge indicates an expected call of Gauge. 59 | func (mr *MockClientMockRecorder) Gauge(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { 60 | mr.mock.ctrl.T.Helper() 61 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Gauge", reflect.TypeOf((*MockClient)(nil).Gauge), arg0, arg1, arg2, arg3) 62 | } 63 | 64 | // TimeInMilliseconds mocks base method. 65 | func (m *MockClient) TimeInMilliseconds(arg0 string, arg1 float64, arg2 []string, arg3 float64) error { 66 | m.ctrl.T.Helper() 67 | ret := m.ctrl.Call(m, "TimeInMilliseconds", arg0, arg1, arg2, arg3) 68 | ret0, _ := ret[0].(error) 69 | return ret0 70 | } 71 | 72 | // TimeInMilliseconds indicates an expected call of TimeInMilliseconds. 73 | func (mr *MockClientMockRecorder) TimeInMilliseconds(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { 74 | mr.mock.ctrl.T.Helper() 75 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TimeInMilliseconds", reflect.TypeOf((*MockClient)(nil).TimeInMilliseconds), arg0, arg1, arg2, arg3) 76 | } 77 | -------------------------------------------------------------------------------- /pkg/metrics/models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Summary defines a summary metric 4 | type Summary struct { 5 | Subsystem string 6 | Name string 7 | Help string 8 | Objectives map[float64]float64 9 | Labels []string 10 | } 11 | 12 | // Histogram defines a histogram metric 13 | type Histogram struct { 14 | Subsystem string 15 | Name string 16 | Help string 17 | Buckets []float64 18 | Labels []string 19 | } 20 | 21 | // Gauge defines a gauge metric 22 | type Gauge struct { 23 | Subsystem string 24 | Name string 25 | Help string 26 | Labels []string 27 | } 28 | 29 | // Counter defines a counter metric 30 | type Counter struct { 31 | Subsystem string 32 | Name string 33 | Help string 34 | Labels []string 35 | } 36 | 37 | // CustomMetricsSpec has all metrics specs 38 | type CustomMetricsSpec struct { 39 | Summaries []*Summary 40 | Histograms []*Histogram 41 | Gauges []*Gauge 42 | Counters []*Counter 43 | } 44 | -------------------------------------------------------------------------------- /pkg/metrics/reporter_interfaces.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package metrics 22 | 23 | // Reporter interface 24 | type Reporter interface { 25 | ReportCount(metric string, tags map[string]string, count float64) error 26 | ReportSummary(metric string, tags map[string]string, value float64) error 27 | ReportHistogram(metric string, tags map[string]string, value float64) error 28 | ReportGauge(metric string, tags map[string]string, value float64) error 29 | } 30 | -------------------------------------------------------------------------------- /pkg/modules/api_docs_gen.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package modules 22 | 23 | import ( 24 | "github.com/topfreegames/pitaya/v3/pkg/component" 25 | "github.com/topfreegames/pitaya/v3/pkg/logger" 26 | ) 27 | 28 | // APIDocsGen is a pitaya module that generates api docs for pitaya servers 29 | type APIDocsGen struct { 30 | Base 31 | basePath string 32 | services []*component.Service 33 | } 34 | 35 | // NewAPIDocsGen creates a new APIDocsGen 36 | func NewAPIDocsGen(basePath string, services []*component.Service) *APIDocsGen { 37 | return &APIDocsGen{ 38 | basePath: basePath, 39 | services: services, 40 | } 41 | } 42 | 43 | // Init is called on init method 44 | func (a *APIDocsGen) Init() error { 45 | for _, s := range a.services { 46 | logger.Log.Infof("loaded svc: %s", s.Name) 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/modules/base.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package modules 22 | 23 | // Base implements a default component for Component. 24 | type Base struct{} 25 | 26 | // Init was called to initialize the component. 27 | func (c *Base) Init() error { 28 | return nil 29 | } 30 | 31 | // AfterInit was called after the component is initialized. 32 | func (c *Base) AfterInit() {} 33 | 34 | // BeforeShutdown was called before the component to shutdown. 35 | func (c *Base) BeforeShutdown() {} 36 | 37 | // Shutdown was called to shutdown the component. 38 | func (c *Base) Shutdown() error { 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/modules/unique_session.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package modules 22 | 23 | import ( 24 | "context" 25 | 26 | "github.com/topfreegames/pitaya/v3/pkg/cluster" 27 | "github.com/topfreegames/pitaya/v3/pkg/session" 28 | ) 29 | 30 | // UniqueSession module watches for sessions using the same UID and kicks them 31 | type UniqueSession struct { 32 | Base 33 | server *cluster.Server 34 | rpcClient cluster.RPCClient 35 | sessionPool session.SessionPool 36 | } 37 | 38 | // NewUniqueSession creates a new unique session module 39 | func NewUniqueSession(server *cluster.Server, rpcServer cluster.RPCServer, rpcClient cluster.RPCClient, sessionPool session.SessionPool) *UniqueSession { 40 | return &UniqueSession{ 41 | server: server, 42 | rpcClient: rpcClient, 43 | sessionPool: sessionPool, 44 | } 45 | } 46 | 47 | // OnUserBind method should be called when a user binds a session in remote servers 48 | func (u *UniqueSession) OnUserBind(uid, fid string) { 49 | if u.server.ID == fid { 50 | return 51 | } 52 | oldSession := u.sessionPool.GetSessionByUID(uid) 53 | if oldSession != nil { 54 | // TODO: it would be nice to set this correctly 55 | oldSession.Kick(context.Background()) 56 | } 57 | } 58 | 59 | // Init initializes the module 60 | func (u *UniqueSession) Init() error { 61 | u.sessionPool.OnSessionBind(func(ctx context.Context, s session.Session) error { 62 | oldSession := u.sessionPool.GetSessionByUID(s.UID()) 63 | if oldSession != nil { 64 | return oldSession.Kick(ctx) 65 | } 66 | err := u.rpcClient.BroadcastSessionBind(s.UID()) 67 | return err 68 | }) 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/networkentity/networkentity.go: -------------------------------------------------------------------------------- 1 | package networkentity 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/topfreegames/pitaya/v3/pkg/protos" 8 | ) 9 | 10 | // NetworkEntity represent low-level network instance 11 | type NetworkEntity interface { 12 | Push(route string, v interface{}) error 13 | ResponseMID(ctx context.Context, mid uint, v interface{}, isError ...bool) error 14 | Close() error 15 | Kick(ctx context.Context) error 16 | RemoteAddr() net.Addr 17 | SendRequest(ctx context.Context, serverID, route string, v interface{}) (*protos.Response, error) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/pipeline/pipeline_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) nano Author and TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package pipeline 22 | 23 | import ( 24 | "context" 25 | "errors" 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | ) 30 | 31 | var ( 32 | handler1 = func(ctx context.Context, in interface{}) (context.Context, interface{}, error) { 33 | return ctx, in, errors.New("ohno") 34 | } 35 | handler2 = func(ctx context.Context, in interface{}) (context.Context, interface{}, error) { 36 | return ctx, nil, nil 37 | } 38 | p = &Channel{} 39 | ) 40 | 41 | func TestPushFront(t *testing.T) { 42 | p.PushFront(handler1) 43 | p.PushFront(handler2) 44 | defer p.Clear() 45 | 46 | _, _, err := p.Handlers[0](nil, nil) 47 | assert.Nil(t, nil, err) 48 | } 49 | 50 | func TestPushBack(t *testing.T) { 51 | p.PushFront(handler1) 52 | p.PushBack(handler2) 53 | defer p.Clear() 54 | 55 | _, _, err := p.Handlers[0](nil, nil) 56 | assert.EqualError(t, errors.New("ohno"), err.Error()) 57 | } 58 | 59 | func TestClear(t *testing.T) { 60 | p.PushFront(handler1) 61 | p.PushBack(handler2) 62 | assert.Len(t, p.Handlers, 2) 63 | p.Clear() 64 | assert.Len(t, p.Handlers, 0) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/protos/doc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: doc.proto 3 | 4 | package protos 5 | 6 | import proto "github.com/golang/protobuf/proto" 7 | import fmt "fmt" 8 | import math "math" 9 | 10 | // Reference imports to suppress errors if they are not otherwise used. 11 | var _ = proto.Marshal 12 | var _ = fmt.Errorf 13 | var _ = math.Inf 14 | 15 | // This is a compile-time assertion to ensure that this generated file 16 | // is compatible with the proto package it is being compiled against. 17 | // A compilation error at this line likely means your copy of the 18 | // proto package needs to be updated. 19 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 20 | 21 | type Doc struct { 22 | Doc string `protobuf:"bytes,1,opt,name=doc" json:"doc,omitempty"` 23 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 24 | XXX_unrecognized []byte `json:"-"` 25 | XXX_sizecache int32 `json:"-"` 26 | } 27 | 28 | func (m *Doc) Reset() { *m = Doc{} } 29 | func (m *Doc) String() string { return proto.CompactTextString(m) } 30 | func (*Doc) ProtoMessage() {} 31 | func (*Doc) Descriptor() ([]byte, []int) { 32 | return fileDescriptor_doc_ddb39afaa46ee6d6, []int{0} 33 | } 34 | func (m *Doc) XXX_Unmarshal(b []byte) error { 35 | return xxx_messageInfo_Doc.Unmarshal(m, b) 36 | } 37 | func (m *Doc) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 38 | return xxx_messageInfo_Doc.Marshal(b, m, deterministic) 39 | } 40 | func (dst *Doc) XXX_Merge(src proto.Message) { 41 | xxx_messageInfo_Doc.Merge(dst, src) 42 | } 43 | func (m *Doc) XXX_Size() int { 44 | return xxx_messageInfo_Doc.Size(m) 45 | } 46 | func (m *Doc) XXX_DiscardUnknown() { 47 | xxx_messageInfo_Doc.DiscardUnknown(m) 48 | } 49 | 50 | var xxx_messageInfo_Doc proto.InternalMessageInfo 51 | 52 | func (m *Doc) GetDoc() string { 53 | if m != nil { 54 | return m.Doc 55 | } 56 | return "" 57 | } 58 | 59 | func init() { 60 | proto.RegisterType((*Doc)(nil), "protos.Doc") 61 | } 62 | 63 | func init() { proto.RegisterFile("doc.proto", fileDescriptor_doc_ddb39afaa46ee6d6) } 64 | 65 | var fileDescriptor_doc_ddb39afaa46ee6d6 = []byte{ 66 | // 66 bytes of a gzipped FileDescriptorProto 67 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4c, 0xc9, 0x4f, 0xd6, 68 | 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x03, 0x53, 0xc5, 0x4a, 0xe2, 0x5c, 0xcc, 0x2e, 0xf9, 69 | 0xc9, 0x42, 0x02, 0x5c, 0xcc, 0x29, 0xf9, 0xc9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x20, 70 | 0x66, 0x12, 0x44, 0x81, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0x98, 0xdb, 0x15, 0x9b, 0x34, 0x00, 71 | 0x00, 0x00, 72 | } 73 | -------------------------------------------------------------------------------- /pkg/protos/docmsg.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: docmsg.proto 3 | 4 | package protos 5 | 6 | import proto "github.com/golang/protobuf/proto" 7 | import fmt "fmt" 8 | import math "math" 9 | 10 | // Reference imports to suppress errors if they are not otherwise used. 11 | var _ = proto.Marshal 12 | var _ = fmt.Errorf 13 | var _ = math.Inf 14 | 15 | // This is a compile-time assertion to ensure that this generated file 16 | // is compatible with the proto package it is being compiled against. 17 | // A compilation error at this line likely means your copy of the 18 | // proto package needs to be updated. 19 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 20 | 21 | type DocMsg struct { 22 | GetProtos bool `protobuf:"varint,1,opt,name=getProtos" json:"getProtos,omitempty"` 23 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 24 | XXX_unrecognized []byte `json:"-"` 25 | XXX_sizecache int32 `json:"-"` 26 | } 27 | 28 | func (m *DocMsg) Reset() { *m = DocMsg{} } 29 | func (m *DocMsg) String() string { return proto.CompactTextString(m) } 30 | func (*DocMsg) ProtoMessage() {} 31 | func (*DocMsg) Descriptor() ([]byte, []int) { 32 | return fileDescriptor_docmsg_6d699a2b0ab5525e, []int{0} 33 | } 34 | func (m *DocMsg) XXX_Unmarshal(b []byte) error { 35 | return xxx_messageInfo_DocMsg.Unmarshal(m, b) 36 | } 37 | func (m *DocMsg) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 38 | return xxx_messageInfo_DocMsg.Marshal(b, m, deterministic) 39 | } 40 | func (dst *DocMsg) XXX_Merge(src proto.Message) { 41 | xxx_messageInfo_DocMsg.Merge(dst, src) 42 | } 43 | func (m *DocMsg) XXX_Size() int { 44 | return xxx_messageInfo_DocMsg.Size(m) 45 | } 46 | func (m *DocMsg) XXX_DiscardUnknown() { 47 | xxx_messageInfo_DocMsg.DiscardUnknown(m) 48 | } 49 | 50 | var xxx_messageInfo_DocMsg proto.InternalMessageInfo 51 | 52 | func (m *DocMsg) GetGetProtos() bool { 53 | if m != nil { 54 | return m.GetProtos 55 | } 56 | return false 57 | } 58 | 59 | func init() { 60 | proto.RegisterType((*DocMsg)(nil), "protos.DocMsg") 61 | } 62 | 63 | func init() { proto.RegisterFile("docmsg.proto", fileDescriptor_docmsg_6d699a2b0ab5525e) } 64 | 65 | var fileDescriptor_docmsg_6d699a2b0ab5525e = []byte{ 66 | // 75 bytes of a gzipped FileDescriptorProto 67 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x49, 0xc9, 0x4f, 0xce, 68 | 0x2d, 0x4e, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x03, 0x53, 0xc5, 0x4a, 0x6a, 0x5c, 69 | 0x6c, 0x2e, 0xf9, 0xc9, 0xbe, 0xc5, 0xe9, 0x42, 0x32, 0x5c, 0x9c, 0xe9, 0xa9, 0x25, 0x01, 0x60, 70 | 0x61, 0x09, 0x46, 0x05, 0x46, 0x0d, 0x8e, 0x20, 0x84, 0x40, 0x12, 0x44, 0xbd, 0x31, 0x20, 0x00, 71 | 0x00, 0xff, 0xff, 0x77, 0x5a, 0xd8, 0xbe, 0x46, 0x00, 0x00, 0x00, 72 | } 73 | -------------------------------------------------------------------------------- /pkg/protos/test/testrequest.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: testrequest.proto 3 | 4 | package test 5 | 6 | import proto "github.com/golang/protobuf/proto" 7 | import fmt "fmt" 8 | import math "math" 9 | 10 | // Reference imports to suppress errors if they are not otherwise used. 11 | var _ = proto.Marshal 12 | var _ = fmt.Errorf 13 | var _ = math.Inf 14 | 15 | // This is a compile-time assertion to ensure that this generated file 16 | // is compatible with the proto package it is being compiled against. 17 | // A compilation error at this line likely means your copy of the 18 | // proto package needs to be updated. 19 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 20 | 21 | type TestRequest struct { 22 | Msg string `protobuf:"bytes,1,opt,name=msg" json:"msg,omitempty"` 23 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 24 | XXX_unrecognized []byte `json:"-"` 25 | XXX_sizecache int32 `json:"-"` 26 | } 27 | 28 | func (m *TestRequest) Reset() { *m = TestRequest{} } 29 | func (m *TestRequest) String() string { return proto.CompactTextString(m) } 30 | func (*TestRequest) ProtoMessage() {} 31 | func (*TestRequest) Descriptor() ([]byte, []int) { 32 | return fileDescriptor_testrequest_64ede5bd0b1b0648, []int{0} 33 | } 34 | func (m *TestRequest) XXX_Unmarshal(b []byte) error { 35 | return xxx_messageInfo_TestRequest.Unmarshal(m, b) 36 | } 37 | func (m *TestRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 38 | return xxx_messageInfo_TestRequest.Marshal(b, m, deterministic) 39 | } 40 | func (dst *TestRequest) XXX_Merge(src proto.Message) { 41 | xxx_messageInfo_TestRequest.Merge(dst, src) 42 | } 43 | func (m *TestRequest) XXX_Size() int { 44 | return xxx_messageInfo_TestRequest.Size(m) 45 | } 46 | func (m *TestRequest) XXX_DiscardUnknown() { 47 | xxx_messageInfo_TestRequest.DiscardUnknown(m) 48 | } 49 | 50 | var xxx_messageInfo_TestRequest proto.InternalMessageInfo 51 | 52 | func (m *TestRequest) GetMsg() string { 53 | if m != nil { 54 | return m.Msg 55 | } 56 | return "" 57 | } 58 | 59 | func init() { 60 | proto.RegisterType((*TestRequest)(nil), "test.TestRequest") 61 | } 62 | 63 | func init() { proto.RegisterFile("testrequest.proto", fileDescriptor_testrequest_64ede5bd0b1b0648) } 64 | 65 | var fileDescriptor_testrequest_64ede5bd0b1b0648 = []byte{ 66 | // 79 bytes of a gzipped FileDescriptorProto 67 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x2c, 0x49, 0x2d, 0x2e, 68 | 0x29, 0x4a, 0x2d, 0x2c, 0x4d, 0x2d, 0x2e, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x01, 69 | 0x09, 0x29, 0xc9, 0x73, 0x71, 0x87, 0xa4, 0x16, 0x97, 0x04, 0x41, 0xa4, 0x84, 0x04, 0xb8, 0x98, 70 | 0x73, 0x8b, 0xd3, 0x25, 0x18, 0x15, 0x18, 0x35, 0x38, 0x83, 0x40, 0xcc, 0x24, 0x36, 0xb0, 0x6a, 71 | 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0x8e, 0xca, 0x49, 0xd7, 0x42, 0x00, 0x00, 0x00, 72 | } 73 | -------------------------------------------------------------------------------- /pkg/reporters.go: -------------------------------------------------------------------------------- 1 | package pitaya 2 | 3 | import ( 4 | "github.com/topfreegames/pitaya/v3/pkg/config" 5 | "github.com/topfreegames/pitaya/v3/pkg/logger" 6 | "github.com/topfreegames/pitaya/v3/pkg/metrics" 7 | "github.com/topfreegames/pitaya/v3/pkg/metrics/models" 8 | ) 9 | 10 | // CreatePrometheusReporter create a Prometheus reporter instance 11 | func CreatePrometheusReporter(serverType string, config config.MetricsConfig, customSpecs models.CustomMetricsSpec) (*metrics.PrometheusReporter, error) { 12 | logger.Log.Infof("prometheus is enabled, configuring reporter on port %d", config.Prometheus.Port) 13 | prometheus, err := metrics.GetPrometheusReporter(serverType, config, customSpecs) 14 | if err != nil { 15 | logger.Log.Errorf("failed to start prometheus metrics reporter, skipping %v", err) 16 | } 17 | return prometheus, err 18 | } 19 | 20 | // CreateStatsdReporter create a Statsd reporter instance 21 | func CreateStatsdReporter(serverType string, config config.MetricsConfig) (*metrics.StatsdReporter, error) { 22 | logger.Log.Infof( 23 | "statsd is enabled, configuring the metrics reporter with host: %s", 24 | config.Statsd.Host, 25 | ) 26 | metricsReporter, err := metrics.NewStatsdReporter( 27 | config, 28 | serverType, 29 | ) 30 | if err != nil { 31 | logger.Log.Errorf("failed to start statds metrics reporter, skipping %v", err) 32 | } 33 | return metricsReporter, err 34 | } 35 | -------------------------------------------------------------------------------- /pkg/route/route.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co and nano Authors. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package route 22 | 23 | import ( 24 | "errors" 25 | "fmt" 26 | "strings" 27 | 28 | "github.com/topfreegames/pitaya/v3/pkg/logger" 29 | ) 30 | 31 | var ( 32 | // ErrRouteFieldCantEmpty error 33 | ErrRouteFieldCantEmpty = errors.New("route field can not be empty") 34 | // ErrInvalidRoute error 35 | ErrInvalidRoute = errors.New("invalid route") 36 | ) 37 | 38 | // Route struct 39 | type Route struct { 40 | SvType string 41 | Service string 42 | Method string 43 | } 44 | 45 | // NewRoute creates a new route 46 | func NewRoute(server, service, method string) *Route { 47 | return &Route{server, service, method} 48 | } 49 | 50 | // String transforms the route into a string 51 | func (r *Route) String() string { 52 | if r.SvType != "" { 53 | return fmt.Sprintf("%s.%s.%s", r.SvType, r.Service, r.Method) 54 | } 55 | return r.Short() 56 | } 57 | 58 | // Short transforms the route into a string without the server type 59 | func (r *Route) Short() string { 60 | return fmt.Sprintf("%s.%s", r.Service, r.Method) 61 | } 62 | 63 | // Decode decodes the route 64 | func Decode(route string) (*Route, error) { 65 | r := strings.Split(route, ".") 66 | for _, s := range r { 67 | if strings.TrimSpace(s) == "" { 68 | return nil, ErrRouteFieldCantEmpty 69 | } 70 | } 71 | switch len(r) { 72 | case 3: 73 | return NewRoute(r[0], r[1], r[2]), nil 74 | case 2: 75 | return NewRoute("", r[0], r[1]), nil 76 | default: 77 | logger.Log.Errorf("invalid route: " + route) 78 | return nil, ErrInvalidRoute 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pkg/serialize/json/json.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) nano Author and TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package json 22 | 23 | import ( 24 | "encoding/json" 25 | ) 26 | 27 | // Serializer implements the serialize.Serializer interface 28 | type Serializer struct{} 29 | 30 | // NewSerializer returns a new Serializer. 31 | func NewSerializer() *Serializer { 32 | return &Serializer{} 33 | } 34 | 35 | // Marshal returns the JSON encoding of v. 36 | func (s *Serializer) Marshal(v interface{}) ([]byte, error) { 37 | return json.Marshal(v) 38 | } 39 | 40 | // Unmarshal parses the JSON-encoded data and stores the result 41 | // in the value pointed to by v. 42 | func (s *Serializer) Unmarshal(data []byte, v interface{}) error { 43 | return json.Unmarshal(data, v) 44 | } 45 | 46 | // GetName returns the name of the serializer. 47 | func (s *Serializer) GetName() string { 48 | return "json" 49 | } 50 | -------------------------------------------------------------------------------- /pkg/serialize/mocks/serializer.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/topfreegames/pitaya/v3/pkg/serialize (interfaces: Serializer) 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | ) 12 | 13 | // MockSerializer is a mock of Serializer interface. 14 | type MockSerializer struct { 15 | ctrl *gomock.Controller 16 | recorder *MockSerializerMockRecorder 17 | } 18 | 19 | // MockSerializerMockRecorder is the mock recorder for MockSerializer. 20 | type MockSerializerMockRecorder struct { 21 | mock *MockSerializer 22 | } 23 | 24 | // NewMockSerializer creates a new mock instance. 25 | func NewMockSerializer(ctrl *gomock.Controller) *MockSerializer { 26 | mock := &MockSerializer{ctrl: ctrl} 27 | mock.recorder = &MockSerializerMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use. 32 | func (m *MockSerializer) EXPECT() *MockSerializerMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // GetName mocks base method. 37 | func (m *MockSerializer) GetName() string { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "GetName") 40 | ret0, _ := ret[0].(string) 41 | return ret0 42 | } 43 | 44 | // GetName indicates an expected call of GetName. 45 | func (mr *MockSerializerMockRecorder) GetName() *gomock.Call { 46 | mr.mock.ctrl.T.Helper() 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockSerializer)(nil).GetName)) 48 | } 49 | 50 | // Marshal mocks base method. 51 | func (m *MockSerializer) Marshal(arg0 interface{}) ([]byte, error) { 52 | m.ctrl.T.Helper() 53 | ret := m.ctrl.Call(m, "Marshal", arg0) 54 | ret0, _ := ret[0].([]byte) 55 | ret1, _ := ret[1].(error) 56 | return ret0, ret1 57 | } 58 | 59 | // Marshal indicates an expected call of Marshal. 60 | func (mr *MockSerializerMockRecorder) Marshal(arg0 interface{}) *gomock.Call { 61 | mr.mock.ctrl.T.Helper() 62 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Marshal", reflect.TypeOf((*MockSerializer)(nil).Marshal), arg0) 63 | } 64 | 65 | // Unmarshal mocks base method. 66 | func (m *MockSerializer) Unmarshal(arg0 []byte, arg1 interface{}) error { 67 | m.ctrl.T.Helper() 68 | ret := m.ctrl.Call(m, "Unmarshal", arg0, arg1) 69 | ret0, _ := ret[0].(error) 70 | return ret0 71 | } 72 | 73 | // Unmarshal indicates an expected call of Unmarshal. 74 | func (mr *MockSerializerMockRecorder) Unmarshal(arg0, arg1 interface{}) *gomock.Call { 75 | mr.mock.ctrl.T.Helper() 76 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unmarshal", reflect.TypeOf((*MockSerializer)(nil).Unmarshal), arg0, arg1) 77 | } 78 | -------------------------------------------------------------------------------- /pkg/serialize/protobuf/fixtures/TestMarshal/test_ok.golden: -------------------------------------------------------------------------------- 1 | 2 | dataerror -------------------------------------------------------------------------------- /pkg/serialize/protobuf/protobuf.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) nano Author and TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package protobuf 22 | 23 | import ( 24 | "github.com/golang/protobuf/proto" 25 | "github.com/topfreegames/pitaya/v3/pkg/constants" 26 | ) 27 | 28 | // Serializer implements the serialize.Serializer interface 29 | type Serializer struct{} 30 | 31 | // NewSerializer returns a new Serializer. 32 | func NewSerializer() *Serializer { 33 | return &Serializer{} 34 | } 35 | 36 | // Marshal returns the protobuf encoding of v. 37 | func (s *Serializer) Marshal(v interface{}) ([]byte, error) { 38 | pb, ok := v.(proto.Message) 39 | if !ok { 40 | return nil, constants.ErrWrongValueType 41 | } 42 | return proto.Marshal(pb) 43 | } 44 | 45 | // Unmarshal parses the protobuf-encoded data and stores the result 46 | // in the value pointed to by v. 47 | func (s *Serializer) Unmarshal(data []byte, v interface{}) error { 48 | pb, ok := v.(proto.Message) 49 | if !ok { 50 | return constants.ErrWrongValueType 51 | } 52 | return proto.Unmarshal(data, pb) 53 | } 54 | 55 | // GetName returns the name of the serializer. 56 | func (s *Serializer) GetName() string { 57 | return "protobuf" 58 | } 59 | -------------------------------------------------------------------------------- /pkg/serialize/serializer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) nano Author and TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package serialize 22 | 23 | import ( 24 | "errors" 25 | 26 | "github.com/topfreegames/pitaya/v3/pkg/serialize/json" 27 | "github.com/topfreegames/pitaya/v3/pkg/serialize/protobuf" 28 | ) 29 | 30 | const ( 31 | JSON Type = 1 32 | PROTOBUF Type = 2 33 | ) 34 | 35 | type ( 36 | // Type is the Serializer type. 37 | Type uint16 38 | 39 | // Marshaler represents a marshal interface 40 | Marshaler interface { 41 | Marshal(interface{}) ([]byte, error) 42 | } 43 | 44 | // Unmarshaler represents a Unmarshal interface 45 | Unmarshaler interface { 46 | Unmarshal([]byte, interface{}) error 47 | } 48 | 49 | // Serializer is the interface that groups the basic Marshal and Unmarshal methods. 50 | Serializer interface { 51 | Marshaler 52 | Unmarshaler 53 | GetName() string 54 | } 55 | ) 56 | 57 | // All recognized and expected serializer type values. 58 | 59 | // NewSerializer returns a new serializer of the respective type (JSON or PROTOBUF) according to serializerType Type. 60 | // If serializerType is a JSON, then a JSON serializer is returned. 61 | // If serializerType is a PROTOBUF, then a PROTOBUF serializer is returned. 62 | // Otherwise, if serializerType is not a valid serializer type, then it returns nil. 63 | func NewSerializer(serializerType Type) (Serializer, error) { //nolint:ireturn 64 | switch serializerType { 65 | case JSON: 66 | return json.NewSerializer(), nil 67 | case PROTOBUF: 68 | return protobuf.NewSerializer(), nil 69 | default: 70 | return nil, errors.New("serializer type unknown") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pkg/service/base_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "github.com/topfreegames/pitaya/v3/pkg/pipeline" 4 | 5 | type baseService struct { 6 | handlerHooks *pipeline.HandlerHooks 7 | } 8 | 9 | func (h *baseService) SetHandlerHooks(handlerHooks *pipeline.HandlerHooks) { 10 | h.handlerHooks = handlerHooks 11 | } 12 | -------------------------------------------------------------------------------- /pkg/service/fixtures/unmarshal_remote_test_1.golden: -------------------------------------------------------------------------------- 1 | blah -------------------------------------------------------------------------------- /pkg/service/fixtures/unmarshal_remote_test_2.golden: -------------------------------------------------------------------------------- 1 | aaa -------------------------------------------------------------------------------- /pkg/service/fixtures/unmarshal_remote_test_3.golden: -------------------------------------------------------------------------------- 1 | aab -------------------------------------------------------------------------------- /pkg/session/fixtures/testSessionSetData_1.golden: -------------------------------------------------------------------------------- 1 | {"byte":"AQ=="} -------------------------------------------------------------------------------- /pkg/session/fixtures/testSessionSetData_2.golden: -------------------------------------------------------------------------------- 1 | {"int":1} -------------------------------------------------------------------------------- /pkg/session/fixtures/testSessionSetData_3.golden: -------------------------------------------------------------------------------- 1 | {"struct":{"A":1,"B":"aaa"}} -------------------------------------------------------------------------------- /pkg/session/fixtures/testSessionSetData_4.golden: -------------------------------------------------------------------------------- 1 | {"string":"aaa"} -------------------------------------------------------------------------------- /pkg/session/fixtures/testSessionSetData_5.golden: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/session/fixtures/testUpdateEncodedData_1.golden: -------------------------------------------------------------------------------- 1 | {"byte":"AQ=="} -------------------------------------------------------------------------------- /pkg/session/fixtures/testUpdateEncodedData_2.golden: -------------------------------------------------------------------------------- 1 | {"int":1} -------------------------------------------------------------------------------- /pkg/session/fixtures/testUpdateEncodedData_3.golden: -------------------------------------------------------------------------------- 1 | {"struct":{"A":1,"B":"aaa"}} -------------------------------------------------------------------------------- /pkg/session/fixtures/testUpdateEncodedData_4.golden: -------------------------------------------------------------------------------- 1 | {"string":"aaa"} -------------------------------------------------------------------------------- /pkg/session/fixtures/testUpdateEncodedData_5.golden: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/session/static.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | var DefaultSessionPool SessionPool 8 | 9 | // GetSessionByUID return a session bound to an user id 10 | func GetSessionByUID(uid string) Session { 11 | return DefaultSessionPool.GetSessionByUID(uid) 12 | } 13 | 14 | // GetSessionByID return a session bound to a frontend server id 15 | func GetSessionByID(id int64) Session { 16 | return DefaultSessionPool.GetSessionByID(id) 17 | } 18 | 19 | // OnSessionBind adds a method to be called when a session is bound 20 | // same function cannot be added twice! 21 | func OnSessionBind(f func(ctx context.Context, s Session) error) { 22 | DefaultSessionPool.OnSessionBind(f) 23 | } 24 | 25 | // OnAfterSessionBind adds a method to be called when session is bound and after all sessionBind callbacks 26 | func OnAfterSessionBind(f func(ctx context.Context, s Session) error) { 27 | DefaultSessionPool.OnAfterSessionBind(f) 28 | } 29 | 30 | // OnSessionClose adds a method that will be called when every session closes 31 | func OnSessionClose(f func(s Session)) { 32 | DefaultSessionPool.OnSessionClose(f) 33 | } 34 | 35 | // CloseAll calls Close on all sessions 36 | func CloseAll() { 37 | DefaultSessionPool.CloseAll() 38 | } 39 | 40 | // GetNumberOfConnectedClients returns the number of connected clients 41 | func GetNumberOfConnectedClients() int64 { 42 | return DefaultSessionPool.GetNumberOfConnectedClients() 43 | } 44 | -------------------------------------------------------------------------------- /pkg/timer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package pitaya 22 | 23 | import ( 24 | "testing" 25 | "time" 26 | 27 | "github.com/stretchr/testify/assert" 28 | "github.com/topfreegames/pitaya/v3/pkg/constants" 29 | "github.com/topfreegames/pitaya/v3/pkg/timer" 30 | ) 31 | 32 | type MyCond struct{} 33 | 34 | func (m *MyCond) Check(now time.Time) bool { 35 | return false 36 | } 37 | 38 | func TestNewTimer(t *testing.T) { 39 | t.Parallel() 40 | tt := NewTimer(100*time.Millisecond, func() {}) 41 | assert.NotNil(t, tt) 42 | } 43 | 44 | func TestNewCountTimer(t *testing.T) { 45 | t.Parallel() 46 | tt := NewCountTimer(100*time.Millisecond, 10, func() {}) 47 | assert.NotNil(t, tt) 48 | } 49 | 50 | func TestNewAfterTimer(t *testing.T) { 51 | t.Parallel() 52 | tt := NewAfterTimer(100*time.Millisecond, func() {}) 53 | assert.NotNil(t, tt) 54 | } 55 | 56 | func TestNewCondTimer(t *testing.T) { 57 | t.Parallel() 58 | _, err := NewCondTimer(nil, func() {}) 59 | assert.EqualError(t, constants.ErrNilCondition, err.Error()) 60 | 61 | tt, err := NewCondTimer(&MyCond{}, func() {}) 62 | assert.NoError(t, err) 63 | assert.NotNil(t, tt) 64 | } 65 | 66 | func TestSetTimerPrecision(t *testing.T) { 67 | t.Parallel() 68 | dur := 33 * time.Millisecond 69 | SetTimerPrecision(dur) 70 | assert.Equal(t, dur, timer.Precision) 71 | } 72 | 73 | func TestSetTimerBacklog(t *testing.T) { 74 | backlog := 1 << 4 75 | SetTimerBacklog(backlog) 76 | } 77 | -------------------------------------------------------------------------------- /pkg/tracing/otel.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/topfreegames/pitaya/v3/pkg/logger" 10 | "go.opentelemetry.io/otel" 11 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace" 12 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" 13 | "go.opentelemetry.io/otel/propagation" 14 | "go.opentelemetry.io/otel/sdk/resource" 15 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 16 | ) 17 | 18 | func InitializeOtel() error { 19 | // Print OpenTelemetry configuration 20 | printOtelConfig() 21 | 22 | ctx := context.Background() 23 | 24 | res, err := resource.New(ctx, 25 | resource.WithFromEnv(), 26 | resource.WithOS(), 27 | resource.WithHost(), 28 | ) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | client := otlptracegrpc.NewClient() 34 | 35 | exporter, err := otlptrace.New(ctx, client) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | tp := sdktrace.NewTracerProvider( 41 | sdktrace.WithBatcher(exporter), 42 | sdktrace.WithResource(res), 43 | ) 44 | 45 | otel.SetTracerProvider(tp) 46 | otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) 47 | 48 | logger.Log.Info("OpenTelemetry initialized using environment variables") 49 | return nil 50 | } 51 | 52 | func printOtelConfig() { 53 | vars := []string{ 54 | "OTEL_SERVICE_NAME", 55 | "OTEL_EXPORTER_OTLP_ENDPOINT", 56 | "OTEL_EXPORTER_OTLP_PROTOCOL", 57 | "OTEL_TRACES_SAMPLER", 58 | "OTEL_TRACES_SAMPLER_ARG", 59 | "OTEL_RESOURCE_ATTRIBUTES", 60 | "OTEL_SDK_DISABLED", 61 | } 62 | 63 | config := make([]string, 0, len(vars)) 64 | for _, v := range vars { 65 | value := os.Getenv(v) 66 | if value == "" { 67 | value = "" 68 | } 69 | config = append(config, fmt.Sprintf("%s=%s", v, value)) 70 | } 71 | 72 | logger.Log.Info(fmt.Sprintf("OpenTelemetry Configuration: %s", strings.Join(config, ", "))) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/util/compression/compression.go: -------------------------------------------------------------------------------- 1 | package compression 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "io" 7 | ) 8 | 9 | func DeflateData(data []byte) ([]byte, error) { 10 | var bb bytes.Buffer 11 | z := zlib.NewWriter(&bb) 12 | _, err := z.Write(data) 13 | if err != nil { 14 | return nil, err 15 | } 16 | z.Close() 17 | return bb.Bytes(), nil 18 | } 19 | 20 | func InflateData(data []byte) ([]byte, error) { 21 | zr, err := zlib.NewReader(bytes.NewBuffer(data)) 22 | if err != nil { 23 | return nil, err 24 | } 25 | defer zr.Close() 26 | 27 | return io.ReadAll(zr) 28 | } 29 | 30 | func IsCompressed(data []byte) bool { 31 | return len(data) > 2 && 32 | ( 33 | // zlib 34 | (data[0] == 0x78 && 35 | (data[1] == 0x9C || 36 | data[1] == 0x01 || 37 | data[1] == 0xDA || 38 | data[1] == 0x5E)) || 39 | // gzip 40 | (data[0] == 0x1F && data[1] == 0x8B)) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/util/compression/compression_test.go: -------------------------------------------------------------------------------- 1 | package compression 2 | 3 | import ( 4 | "flag" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "github.com/topfreegames/pitaya/v3/pkg/helpers" 11 | ) 12 | 13 | var update = flag.Bool("update", false, "update .golden files") 14 | 15 | var ins = []struct { 16 | name string 17 | data string 18 | }{ 19 | {"compression_deflate_test_1", "test"}, 20 | {"compression_deflate_test_2", "{a:1,b:2}"}, 21 | {"compression_deflate_test_3", "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit"}, 22 | } 23 | 24 | func TestCompressionDeflate(t *testing.T) { 25 | for _, in := range ins { 26 | t.Run(in.name, func(t *testing.T) { 27 | b, err := DeflateData([]byte(in.data)) 28 | require.NoError(t, err) 29 | gp := filepath.Join("fixtures", in.name+".golden") 30 | if *update { 31 | t.Log("updating golden file") 32 | helpers.WriteFile(t, gp, b) 33 | } 34 | expected := helpers.ReadFile(t, gp) 35 | 36 | assert.Equal(t, expected, b) 37 | }) 38 | } 39 | } 40 | 41 | func TestCompressionInflate(t *testing.T) { 42 | for _, in := range ins { 43 | t.Run(in.name, func(t *testing.T) { 44 | inputFile := filepath.Join("fixtures", in.name+".golden") 45 | input := helpers.ReadFile(t, inputFile) 46 | 47 | result, err := InflateData(input) 48 | require.NoError(t, err) 49 | 50 | assert.Equal(t, string(result), in.data) 51 | }) 52 | } 53 | } 54 | 55 | func TestCompressionInflateIncorrectData(t *testing.T) { 56 | t.Run("compression_deflate_incorrect_data", func(t *testing.T) { 57 | input := "arbitrary data" 58 | 59 | result, err := InflateData([]byte(input)) 60 | require.Error(t, err) 61 | assert.Nil(t, result) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/util/compression/fixtures/compression_deflate_test_1.golden: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfreegames/pitaya/08d34a00bfc535b4e280cf74fd1092562375402d/pkg/util/compression/fixtures/compression_deflate_test_1.golden -------------------------------------------------------------------------------- /pkg/util/compression/fixtures/compression_deflate_test_2.golden: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfreegames/pitaya/08d34a00bfc535b4e280cf74fd1092562375402d/pkg/util/compression/fixtures/compression_deflate_test_2.golden -------------------------------------------------------------------------------- /pkg/util/compression/fixtures/compression_deflate_test_3.golden: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfreegames/pitaya/08d34a00bfc535b4e280cf74fd1092562375402d/pkg/util/compression/fixtures/compression_deflate_test_3.golden -------------------------------------------------------------------------------- /pkg/util/fixtures/gob_encode_test_1.golden: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfreegames/pitaya/08d34a00bfc535b4e280cf74fd1092562375402d/pkg/util/fixtures/gob_encode_test_1.golden -------------------------------------------------------------------------------- /pkg/util/fixtures/gob_encode_test_2.golden: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfreegames/pitaya/08d34a00bfc535b4e280cf74fd1092562375402d/pkg/util/fixtures/gob_encode_test_2.golden -------------------------------------------------------------------------------- /pkg/util/fixtures/gob_encode_test_3.golden: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfreegames/pitaya/08d34a00bfc535b4e280cf74fd1092562375402d/pkg/util/fixtures/gob_encode_test_3.golden -------------------------------------------------------------------------------- /pkg/worker/constants.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package worker 22 | 23 | const ( 24 | rpcQueue = "rpc" 25 | class = "" 26 | ) 27 | -------------------------------------------------------------------------------- /pkg/worker/models.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package worker 22 | 23 | import "github.com/golang/protobuf/proto" 24 | 25 | type rpcInfo struct { 26 | Route string 27 | Metadata map[string]interface{} 28 | Arg proto.Message 29 | Reply proto.Message 30 | } 31 | 32 | type rpcRoute struct { 33 | Route string 34 | } 35 | -------------------------------------------------------------------------------- /pkg/worker/report.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/topfreegames/pitaya/v3/pkg/logger" 8 | "github.com/topfreegames/pitaya/v3/pkg/metrics" 9 | 10 | workers "github.com/topfreegames/go-workers" 11 | ) 12 | 13 | // Report sends periodic of worker reports 14 | func Report(reporters []metrics.Reporter, period time.Duration) { 15 | for { 16 | time.Sleep(period) 17 | 18 | workerStats := workers.GetStats() 19 | for _, r := range reporters { 20 | reportJobsRetry(r, workerStats.Retries) 21 | reportQueueSizes(r, workerStats.Enqueued) 22 | reportJobsTotal(r, workerStats.Failed, workerStats.Processed) 23 | } 24 | } 25 | } 26 | 27 | func reportJobsRetry(r metrics.Reporter, retries int64) { 28 | err := r.ReportGauge(metrics.WorkerJobsRetry, map[string]string{}, float64(retries)) 29 | checkReportErr(metrics.WorkerJobsRetry, err) 30 | } 31 | 32 | func reportQueueSizes(r metrics.Reporter, queues map[string]string) { 33 | for queue, size := range queues { 34 | tags := map[string]string{"queue": queue} 35 | sizeFlt, err := strconv.ParseFloat(size, 64) 36 | if err != nil { 37 | logger.Log.Errorf("queue size is not int: queue=%s size=%s", queue, size) 38 | continue 39 | } 40 | 41 | err = r.ReportGauge(metrics.WorkerQueueSize, tags, sizeFlt) 42 | checkReportErr(metrics.WorkerQueueSize, err) 43 | } 44 | } 45 | 46 | func reportJobsTotal(r metrics.Reporter, failed, processed int) { 47 | // "failed" and "processed" always grow up, 48 | // so they work as count but must be reported as gauge 49 | err := r.ReportGauge(metrics.WorkerJobsTotal, map[string]string{ 50 | "status": "failed", 51 | }, float64(failed)) 52 | checkReportErr(metrics.WorkerJobsTotal, err) 53 | 54 | err = r.ReportGauge(metrics.WorkerJobsTotal, map[string]string{ 55 | "status": "ok", 56 | }, float64(processed)) 57 | checkReportErr(metrics.WorkerJobsTotal, err) 58 | } 59 | 60 | func checkReportErr(metric string, err error) { 61 | if err != nil { 62 | logger.Log.Errorf("failed to report to %s: %q\n", metric, err) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/worker/report_test.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/golang/mock/gomock" 8 | "github.com/topfreegames/pitaya/v3/pkg/metrics" 9 | "github.com/topfreegames/pitaya/v3/pkg/metrics/mocks" 10 | ) 11 | 12 | func TestReportJobsRetry(t *testing.T) { 13 | t.Parallel() 14 | 15 | t.Run("success", func(t *testing.T) { 16 | ctrl := gomock.NewController(t) 17 | defer ctrl.Finish() 18 | 19 | mockReporter := mocks.NewMockReporter(ctrl) 20 | mockReporter.EXPECT().ReportGauge( 21 | metrics.WorkerJobsRetry, 22 | map[string]string{}, 23 | float64(10)) 24 | 25 | reportJobsRetry(mockReporter, 10) 26 | }) 27 | 28 | t.Run("error", func(t *testing.T) { 29 | ctrl := gomock.NewController(t) 30 | defer ctrl.Finish() 31 | 32 | mockReporter := mocks.NewMockReporter(ctrl) 33 | mockReporter.EXPECT().ReportGauge( 34 | metrics.WorkerJobsRetry, 35 | map[string]string{}, 36 | float64(10)).Return(errors.New("err")) 37 | 38 | reportJobsRetry(mockReporter, 10) 39 | }) 40 | } 41 | 42 | func TestReportQueueSizes(t *testing.T) { 43 | t.Parallel() 44 | 45 | var ( 46 | queue1 = "queuename1" 47 | queue2 = "queuename2" 48 | ) 49 | 50 | ctrl := gomock.NewController(t) 51 | defer ctrl.Finish() 52 | 53 | mockReporter := mocks.NewMockReporter(ctrl) 54 | mockReporter.EXPECT().ReportGauge( 55 | metrics.WorkerQueueSize, 56 | map[string]string{"queue": queue1}, 57 | float64(10)) 58 | mockReporter.EXPECT().ReportGauge( 59 | metrics.WorkerQueueSize, 60 | map[string]string{"queue": queue2}, 61 | float64(20)) 62 | 63 | reportQueueSizes(mockReporter, map[string]string{ 64 | queue1: "10", 65 | queue2: "20", 66 | }) 67 | } 68 | 69 | func TestReportJobsTotal(t *testing.T) { 70 | t.Parallel() 71 | 72 | ctrl := gomock.NewController(t) 73 | defer ctrl.Finish() 74 | 75 | mockReporter := mocks.NewMockReporter(ctrl) 76 | mockReporter.EXPECT().ReportGauge( 77 | metrics.WorkerJobsTotal, 78 | map[string]string{"status": "failed"}, 79 | float64(10)) 80 | 81 | mockReporter.EXPECT().ReportGauge( 82 | metrics.WorkerJobsTotal, 83 | map[string]string{"status": "ok"}, 84 | float64(20)) 85 | 86 | reportJobsTotal(mockReporter, 10, 20) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/worker/rpc_job.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package worker 22 | 23 | import ( 24 | "context" 25 | 26 | "github.com/golang/protobuf/proto" 27 | ) 28 | 29 | // RPCJob has infos to execute a rpc on worker 30 | type RPCJob interface { 31 | // ServerDiscovery returns a serverID based on the route 32 | // and any metadata that is necessary to decide 33 | ServerDiscovery( 34 | route string, 35 | rpcMetadata map[string]interface{}, 36 | ) (serverID string, err error) 37 | 38 | // RPC executes the RPC 39 | // It is expected that if serverID is "" the RPC 40 | // happens to any destiny server 41 | RPC( 42 | ctx context.Context, 43 | serverID, routeStr string, 44 | reply, arg proto.Message, 45 | ) error 46 | 47 | // GetArgReply returns the arg and reply of the 48 | // method 49 | GetArgReply(route string) (arg, reply proto.Message, err error) 50 | } 51 | -------------------------------------------------------------------------------- /repl/file.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package repl 22 | 23 | import ( 24 | "bufio" 25 | "errors" 26 | "log" 27 | "os" 28 | "strings" 29 | ) 30 | 31 | func executeFromFile(fileName string) { 32 | logger := log.New(os.Stdout, "", log.LstdFlags) 33 | 34 | var err error 35 | defer func() { 36 | if err != nil { 37 | logger.Printf("error: %s", err.Error()) 38 | } 39 | }() 40 | 41 | var file *os.File 42 | file, err = os.Open(fileName) 43 | if err != nil { 44 | return 45 | } 46 | 47 | scanner := bufio.NewScanner(file) 48 | for scanner.Scan() { 49 | command := scanner.Text() 50 | err = executeCommand(logger, command) 51 | if err != nil { 52 | return 53 | } 54 | } 55 | 56 | err = scanner.Err() 57 | if err != nil { 58 | return 59 | } 60 | } 61 | 62 | func executeCommand(logger Log, command string) error { 63 | parts := strings.Split(command, " ") 64 | 65 | switch parts[0] { 66 | case "connect": 67 | return connect(logger, parts[1], func(data []byte) { 68 | log.Printf("sv-> %s\n", string(data)) 69 | wait.Done() 70 | }) 71 | 72 | case "sethandshake": 73 | return setHandshake(logger, parts[1:]) 74 | 75 | case "request": 76 | wait.Add(1) 77 | if err := request(logger, parts[1:]); err != nil { 78 | return err 79 | } 80 | wait.Wait() 81 | return nil 82 | 83 | case "notify": 84 | return notify(logger, parts[1:]) 85 | 86 | case "push": 87 | return push(logger, parts[1:]) 88 | case "routes": 89 | return routes(logger) 90 | case "disconnect": 91 | disconnect() 92 | return nil 93 | 94 | default: 95 | return errors.New("command not found") 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /repl/log.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package repl 22 | 23 | // Log has log methods 24 | type Log interface { 25 | Print(...interface{}) 26 | Println(...interface{}) 27 | Printf(string, ...interface{}) 28 | } 29 | -------------------------------------------------------------------------------- /repl/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) TFG Co. All Rights Reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package repl 22 | 23 | import ( 24 | "sync" 25 | 26 | "github.com/topfreegames/pitaya/v3/pkg/client" 27 | "github.com/topfreegames/pitaya/v3/pkg/session" 28 | ) 29 | 30 | var ( 31 | pClient client.PitayaClient 32 | disconnectedCh chan bool 33 | docsString string 34 | fileName string 35 | pushInfo map[string]string 36 | wait sync.WaitGroup 37 | prettyJSON bool 38 | handshake *session.HandshakeData 39 | ) 40 | 41 | func Start(docs, filename string, prettyJSON bool) { 42 | docsString = docs 43 | fileName = filename 44 | prettyJSON = prettyJSON 45 | handshake = &session.HandshakeData{ 46 | Sys: session.HandshakeClientData{ 47 | Platform: "repl", 48 | LibVersion: "0.3.5-release", 49 | BuildNumber: "20", 50 | Version: "1.0.0", 51 | }, 52 | User: map[string]interface{}{ 53 | "client": "repl", 54 | }, 55 | } 56 | 57 | switch { 58 | case fileName != "": 59 | executeFromFile(fileName) 60 | default: 61 | repl() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /xk6-pitaya/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the k6 binary with the extension 2 | FROM golang:1.23 as builder 3 | 4 | ARG pitaya_revision 5 | 6 | RUN go install go.k6.io/xk6/cmd/xk6@latest 7 | # For our example, we'll add support for output of test metrics to InfluxDB v2. 8 | # Feel free to add other extensions using the '--with ...'. 9 | RUN xk6 build \ 10 | --with github.com/topfreegames/pitaya/xk6-pitaya@$pitaya_revision \ 11 | --replace github.com/topfreegames/pitaya/v3=github.com/topfreegames/pitaya/v3@$pitaya_revision \ 12 | --output /k6 13 | 14 | # Use the operator's base image and override the k6 binary 15 | FROM grafana/k6:latest 16 | COPY --from=builder /k6 /usr/bin/k6 17 | -------------------------------------------------------------------------------- /xk6-pitaya/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Wildlife Studios 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /xk6-pitaya/examples/scenario1.js: -------------------------------------------------------------------------------- 1 | import pitaya from 'k6/x/pitaya'; 2 | import { check } from 'k6'; 3 | 4 | export const options = { 5 | vus: 10, 6 | duration: '10s', 7 | } 8 | 9 | const opts = { 10 | handshakeData: { 11 | sys: { 12 | clientVersion: "1.0.1", 13 | clientBuildNumber: "1", 14 | platform: "android" 15 | }, 16 | user: { 17 | fiu: "c0a78b27-dd34-4e0d-bff7-36168fce0df5", 18 | bundleId: "com.game.test", 19 | deviceType: "ios", 20 | language: "en", 21 | osVersion: "12.0", 22 | region: "US", 23 | stack: "green-stack" 24 | } 25 | }, 26 | requestTimeoutMs: 1000, 27 | useTLS: false, 28 | } 29 | 30 | const pitayaClient = new pitaya.Client(opts) 31 | 32 | export default async () => { 33 | if (!pitayaClient.isConnected()) { 34 | pitayaClient.connect("localhost:3250") 35 | } 36 | 37 | check(pitayaClient.isConnected(), { 'pitaya client is connected': (r) => r === true }) 38 | 39 | var res = await pitayaClient.request("room.room.entry") 40 | check(res.result, { 'contains an result field': (r) => r !== undefined }) 41 | check(res.result, { 'result is ok': (r) => r === "ok" }) 42 | var res = await pitayaClient.request("room.room.setsessiondata", { data: {"testKey": "testVal"} }) 43 | check(res, { 'res is success': (r) => String.fromCharCode.apply(null,r) === "success"} ) 44 | var res = await pitayaClient.request("room.room.getsessiondata") 45 | check(res.Data, { 'res contains set data': (r) => r.testKey === "testVal"} ) 46 | 47 | pitayaClient.notify("room.room.notifypush") 48 | res = await pitayaClient.consumePush("testPush", 100) 49 | check(res.Msg, { 'push contains msg': (m) => m === "test"} ) 50 | 51 | res = await pitayaClient.request("room.room.join") 52 | check(res.result, { 'result from join is successful': (r) => r === "success"} ) 53 | res = await pitayaClient.consumePush("onMembers", 1000) 54 | check(res.Members, { 'res contains a member group': (m) => m !== undefined } ) 55 | res = await pitayaClient.request("room.room.leave") 56 | check(res, { 'result from leave is successful': (r) => String.fromCharCode.apply(null,r) === "success"}) 57 | 58 | pitayaClient.disconnect() 59 | } 60 | 61 | export function teardown() { 62 | } 63 | -------------------------------------------------------------------------------- /xk6-pitaya/register.go: -------------------------------------------------------------------------------- 1 | // Package pitays only exists to register the pitaya extension 2 | package pitaya 3 | 4 | import ( 5 | "go.k6.io/k6/js/modules" 6 | ) 7 | 8 | // Register the extension on module initialization, available to 9 | // import from JS as "k6/x/pitaya". 10 | func init() { 11 | modules.Register("k6/x/pitaya", new(RootModule)) 12 | } 13 | -------------------------------------------------------------------------------- /xk6-pitaya/stats.go: -------------------------------------------------------------------------------- 1 | package pitaya 2 | 3 | import ( 4 | "errors" 5 | 6 | "go.k6.io/k6/js/modules" 7 | "go.k6.io/k6/metrics" 8 | ) 9 | 10 | type pitayaMetrics struct { 11 | RequestResponseTime *metrics.Metric 12 | TimeoutRequests *metrics.Metric 13 | TagsAndMeta *metrics.TagsAndMeta 14 | } 15 | 16 | // registerMetrics registers the metrics for the mqtt module in the metrics registry 17 | func registerMetrics(vu modules.VU) (pitayaMetrics, error) { 18 | p := pitayaMetrics{} 19 | env := vu.InitEnv() 20 | if env == nil { 21 | return p, errors.New("missing env") 22 | } 23 | registry := env.Registry 24 | if registry == nil { 25 | return p, errors.New("missing registry") 26 | } 27 | 28 | var err error 29 | p.RequestResponseTime, err = registry.NewMetric("pitaya_client_request_duration_ms", metrics.Trend) 30 | if err != nil { 31 | return p, err 32 | } 33 | 34 | p.TimeoutRequests, err = registry.NewMetric("pitaya_client_request_timeout_count", metrics.Counter) 35 | if err != nil { 36 | return p, err 37 | } 38 | 39 | p.TagsAndMeta = &metrics.TagsAndMeta{ 40 | Tags: registry.RootTagSet(), 41 | } 42 | return p, nil 43 | } 44 | --------------------------------------------------------------------------------