├── .dockerignore ├── .github └── workflows │ ├── benchmark.template.yaml │ ├── decide-runner.template.yaml │ ├── docker-bake.template.yaml │ ├── federation-v1.workflow.yaml │ ├── images-cleanup.yaml │ └── report.template.yaml ├── .gitignore ├── LICENSE ├── README.md ├── docker-compose.metrics.yaml ├── federation ├── gateways │ ├── apollo-router │ │ ├── config.yaml │ │ ├── docker-compose.yaml │ │ └── supergraph.graphql │ ├── apollo-server │ │ ├── Dockerfile │ │ ├── docker-compose.yaml │ │ ├── index.ts │ │ ├── package.json │ │ ├── supergraph.graphql │ │ └── yarn.lock │ ├── cosmo │ │ ├── Dockerfile │ │ ├── accounts.graphql │ │ ├── docker-compose.yaml │ │ ├── graph.yaml │ │ ├── inventory.graphql │ │ ├── products.graphql │ │ ├── reviews.graphql │ │ └── router.json │ ├── grafbase │ │ ├── Dockerfile │ │ ├── docker-compose.yaml │ │ └── supergraph.graphql │ ├── hive-gateway-bun │ │ ├── docker-compose.yaml │ │ └── supergraph.graphql │ ├── hive-gateway │ │ ├── docker-compose.yaml │ │ └── supergraph.graphql │ └── mercurius │ │ ├── .yarnrc │ │ ├── Dockerfile │ │ ├── docker-compose.yaml │ │ ├── index.ts │ │ ├── package.json │ │ └── yarn.lock ├── scenarios │ ├── compose-supergraph.ts │ ├── constant-vus-over-time │ │ ├── README.md │ │ ├── benchmark.k6.js │ │ ├── generate-report.ts │ │ ├── package.json │ │ ├── patches │ │ │ └── vega-canvas+2.0.0.patch │ │ ├── run.sh │ │ └── yarn.lock │ ├── constant-vus-subgraphs-delay-resources │ │ └── README.md │ ├── constant-vus-subgraphs-delay │ │ └── README.md │ ├── k6.shared.js │ ├── package.json │ ├── ramping-vus │ │ ├── README.md │ │ ├── benchmark.k6.js │ │ ├── generate-report.ts │ │ ├── package.json │ │ ├── patches │ │ │ └── vega-canvas+2.0.0.patch │ │ └── run.sh │ └── yarn.lock └── subgraphs │ ├── Dockerfile.rust │ ├── accounts │ ├── Cargo.lock │ ├── Cargo.toml │ └── subgraph.rs │ ├── docker-compose.subgraphs.local.yaml │ ├── docker-compose.subgraphs.yaml │ ├── docker.hcl │ ├── inventory │ ├── Cargo.lock │ ├── Cargo.toml │ └── subgraph.rs │ ├── products │ ├── Cargo.lock │ ├── Cargo.toml │ └── subgraph.rs │ └── reviews │ ├── Cargo.lock │ ├── Cargo.toml │ └── subgraph.rs ├── grafana ├── dashboards │ └── dashboard.yaml ├── datasources │ └── datasource.yaml └── json-dashboards │ ├── cadvisor-exporter.json │ └── test-result_rev2.json ├── prometheus └── prometheus.yaml ├── renovate.json └── scripts └── images-cleanup.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/node_modules/** 4 | target 5 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.template.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | baseDir: 5 | type: string 6 | required: true 7 | runner: 8 | type: string 9 | required: true 10 | scenarioDir: 11 | type: string 12 | required: true 13 | scenarioName: 14 | type: string 15 | required: true 16 | gateway: 17 | type: string 18 | required: true 19 | vu: 20 | type: number 21 | required: true 22 | time: 23 | type: string 24 | required: true 25 | cpuLimit: 26 | type: string 27 | required: true 28 | fork: 29 | type: string 30 | required: true 31 | memoryLimit: 32 | type: string 33 | required: true 34 | subgraphDelayRange: 35 | type: string 36 | default: "" 37 | 38 | jobs: 39 | test: 40 | runs-on: ${{ startsWith(inputs.runner, '{') && fromJSON(inputs.runner) || inputs.runner }} 41 | name: ${{ inputs.gateway }} 42 | steps: 43 | - name: adjust os 44 | run: | 45 | ulimit -n 10000 46 | 47 | - name: checkout 48 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 49 | 50 | - name: Setup K6 51 | run: | 52 | sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 53 | echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list 54 | sudo apt-get update 55 | sudo apt-get install k6=0.49.0 56 | 57 | - name: login to docker registry 58 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 59 | with: 60 | registry: ghcr.io 61 | username: ${{ github.actor }} 62 | password: ${{ secrets.GITHUB_TOKEN }} 63 | 64 | - name: prepare containers and run benchmark (${{ inputs.time }}) 65 | timeout-minutes: 20 66 | working-directory: ${{ inputs.baseDir }}/scenarios/${{ inputs.scenarioDir }} 67 | run: ./run.sh ${{ inputs.gateway }} 68 | env: 69 | ACCOUNTS_SUBGRAPH_DELAY_MS: ${{ inputs.subgraphDelayRange }} 70 | INVENTORY_SUBGRAPH_DELAY_MS: ${{ inputs.subgraphDelayRange }} 71 | PRODUCTS_SUBGRAPH_DELAY_MS: ${{ inputs.subgraphDelayRange }} 72 | REVIEWS_SUBGRAPH_DELAY_MS: ${{ inputs.subgraphDelayRange }} 73 | DOCKER_REGISTRY: ghcr.io/${{ github.repository }}/ 74 | DOCKER_TAG: ":${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}" 75 | BENCH_VUS: ${{ inputs.vu }} 76 | BENCH_OVER_TIME: ${{ inputs.time }} 77 | MEM_LIMIT: ${{ inputs.memoryLimit }} 78 | CPU_LIMIT: ${{ inputs.cpuLimit }} 79 | FORK: ${{ inputs.fork }} 80 | 81 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 82 | if: always() 83 | with: 84 | name: ${{ inputs.baseDir }}_${{ inputs.scenarioName }}_${{ inputs.gateway }} 85 | path: | 86 | ${{ inputs.baseDir }}/gateways/${{ inputs.gateway }}/k6_summary.json 87 | ${{ inputs.baseDir }}/gateways/${{ inputs.gateway }}/k6_summary.txt 88 | ${{ inputs.baseDir }}/gateways/${{ inputs.gateway }}/gateway_log.txt 89 | ${{ inputs.baseDir }}/gateways/${{ inputs.gateway }}/*.png 90 | -------------------------------------------------------------------------------- /.github/workflows/decide-runner.template.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | outputs: 4 | runner: 5 | value: ${{ jobs.setup.outputs.runner }} 6 | 7 | jobs: 8 | setup: 9 | runs-on: ubuntu-24.04 10 | outputs: 11 | runner: ${{ steps.step1.outputs.runner }} 12 | name: setup 13 | steps: 14 | - name: decide on runner 15 | id: step1 16 | run: | 17 | if [ ${{ github.event_name }} == 'pull_request' ]; then 18 | echo "using GitHub Actions default runner" 19 | echo "runner=ubuntu-22.04" >> $GITHUB_OUTPUT 20 | else 21 | echo "using stable runner: 'benchmark-runner-1'" 22 | echo 'runner=benchmark-runner-1' >> $GITHUB_OUTPUT 23 | fi 24 | -------------------------------------------------------------------------------- /.github/workflows/docker-bake.template.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | hcl: 5 | type: string 6 | required: true 7 | dir: 8 | type: string 9 | required: true 10 | target: 11 | type: string 12 | required: true 13 | 14 | jobs: 15 | bake: 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - name: checkout 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 20 | 21 | - name: configure eqemu 22 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3 23 | with: 24 | platforms: 'linux/arm64,linux/amd64' 25 | 26 | - name: configure docker buildx 27 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 28 | 29 | - name: login to docker registry 30 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 31 | with: 32 | registry: ghcr.io 33 | username: ${{ github.actor }} 34 | password: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: build docker images 37 | timeout-minutes: 20 38 | id: docker-bake 39 | uses: docker/bake-action@37816e747588cb137173af99ab33873600c46ea8 # v6 40 | env: 41 | DOCKER_REGISTRY: ghcr.io/${{ github.repository }}/ 42 | COMMIT_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} 43 | with: 44 | source: . 45 | workdir: ${{ inputs.dir }} 46 | provenance: false 47 | push: true 48 | files: ${{ inputs.hcl }} 49 | targets: ${{ inputs.target }} 50 | set: | 51 | *.cache-from=type=gha,scope=build 52 | *.cache-to=type=gha,scope=build,mode=max -------------------------------------------------------------------------------- /.github/workflows/federation-v1.workflow.yaml: -------------------------------------------------------------------------------- 1 | name: federation-v1 2 | on: 3 | pull_request: {} 4 | workflow_dispatch: {} 5 | 6 | concurrency: 7 | group: federation-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | subgraphs: 12 | uses: ./.github/workflows/docker-bake.template.yaml 13 | with: 14 | dir: ./federation/subgraphs 15 | hcl: docker.hcl 16 | target: subgraphs 17 | 18 | decide-runner: 19 | needs: subgraphs 20 | uses: ./.github/workflows/decide-runner.template.yaml 21 | 22 | constant-vus-over-time: 23 | needs: 24 | - decide-runner 25 | - subgraphs 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | directory: 30 | - apollo-server 31 | - apollo-router 32 | - hive-gateway 33 | - hive-gateway-bun 34 | - cosmo 35 | - mercurius 36 | - grafbase 37 | uses: ./.github/workflows/benchmark.template.yaml 38 | with: 39 | gateway: ${{ matrix.directory }} 40 | vu: 300 41 | time: ${{ github.event_name == 'pull_request' && '30s' || '60s' }} 42 | baseDir: federation 43 | scenarioDir: constant-vus-over-time 44 | scenarioName: constant-vus-over-time 45 | runner: ${{ needs.decide-runner.outputs.runner }} 46 | cpuLimit: 3 47 | fork: 2 48 | memoryLimit: 6gb 49 | 50 | constant-vus-over-time-report: 51 | needs: constant-vus-over-time 52 | uses: ./.github/workflows/report.template.yaml 53 | secrets: inherit 54 | with: 55 | scenarioName: constant-vus-over-time 56 | baseDir: federation 57 | scenarioDir: constant-vus-over-time 58 | 59 | constant-vus-subgraphs-delay: 60 | needs: 61 | - decide-runner 62 | - subgraphs 63 | strategy: 64 | fail-fast: false 65 | matrix: 66 | directory: 67 | - apollo-server 68 | - apollo-router 69 | - hive-gateway 70 | - hive-gateway-bun 71 | - cosmo 72 | - mercurius 73 | - grafbase 74 | uses: ./.github/workflows/benchmark.template.yaml 75 | with: 76 | gateway: ${{ matrix.directory }} 77 | vu: 300 78 | time: ${{ github.event_name == 'pull_request' && '30s' || '60s' }} 79 | baseDir: federation 80 | scenarioDir: constant-vus-over-time 81 | scenarioName: constant-vus-subgraphs-delay 82 | runner: ${{ needs.decide-runner.outputs.runner }} 83 | cpuLimit: 3 84 | fork: 2 85 | memoryLimit: 6gb 86 | subgraphDelayRange: "40~150" 87 | 88 | constant-vus-subgraphs-delay-report: 89 | needs: constant-vus-subgraphs-delay 90 | uses: ./.github/workflows/report.template.yaml 91 | secrets: inherit 92 | with: 93 | scenarioName: constant-vus-subgraphs-delay 94 | baseDir: federation 95 | scenarioDir: constant-vus-over-time 96 | 97 | constant-vus-subgraphs-delay-resources: 98 | needs: 99 | - decide-runner 100 | - subgraphs 101 | strategy: 102 | fail-fast: false 103 | matrix: 104 | directory: 105 | - apollo-server 106 | - apollo-router 107 | - hive-gateway 108 | - hive-gateway-bun 109 | - cosmo 110 | - mercurius 111 | - grafbase 112 | uses: ./.github/workflows/benchmark.template.yaml 113 | with: 114 | gateway: ${{ matrix.directory }} 115 | vu: 500 116 | time: ${{ github.event_name == 'pull_request' && '30s' || '60s' }} 117 | baseDir: federation 118 | scenarioDir: constant-vus-over-time 119 | scenarioName: constant-vus-subgraphs-delay-resources 120 | runner: ${{ needs.decide-runner.outputs.runner }} 121 | cpuLimit: 4 122 | fork: 3 123 | memoryLimit: 8gb 124 | subgraphDelayRange: "40~150" 125 | 126 | constant-vus-subgraphs-delay-resources-report: 127 | needs: constant-vus-subgraphs-delay-resources 128 | uses: ./.github/workflows/report.template.yaml 129 | secrets: inherit 130 | with: 131 | scenarioName: constant-vus-subgraphs-delay-resources 132 | baseDir: federation 133 | scenarioDir: constant-vus-over-time 134 | 135 | ramping-vus: 136 | needs: 137 | - decide-runner 138 | - subgraphs 139 | strategy: 140 | fail-fast: false 141 | matrix: 142 | directory: 143 | - apollo-server 144 | - apollo-router 145 | - hive-gateway 146 | - hive-gateway-bun 147 | - cosmo 148 | - mercurius 149 | - grafbase 150 | uses: ./.github/workflows/benchmark.template.yaml 151 | with: 152 | gateway: ${{ matrix.directory }} 153 | vu: 2000 154 | time: 60s 155 | baseDir: federation 156 | scenarioDir: ramping-vus 157 | scenarioName: ramping-vus 158 | runner: ${{ needs.decide-runner.outputs.runner }} 159 | cpuLimit: 4 160 | fork: 3 161 | memoryLimit: 8gb 162 | 163 | ramping-vus-report: 164 | needs: ramping-vus 165 | uses: ./.github/workflows/report.template.yaml 166 | secrets: inherit 167 | with: 168 | scenarioName: ramping-vus 169 | scenarioDir: ramping-vus 170 | baseDir: federation 171 | -------------------------------------------------------------------------------- /.github/workflows/images-cleanup.yaml: -------------------------------------------------------------------------------- 1 | name: 'Images Cleanup' 2 | 3 | on: 4 | schedule: 5 | # Run every hour 6 | - cron: '0 * * * *' 7 | workflow_dispatch: {} 8 | 9 | jobs: 10 | report: 11 | runs-on: ubuntu-24.04 12 | name: report 13 | steps: 14 | - name: checkout 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 16 | 17 | - name: setup node 18 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 19 | with: 20 | node-version: 22.6 21 | 22 | # run script with 30m timeout 23 | - name: cleanup images 24 | env: 25 | CF_IMAGES_LINK: ${{ secrets.CF_IMAGES_LINK }} 26 | CF_IMAGES_TOKEN: ${{ secrets.CF_IMAGES_TOKEN }} 27 | run: node --experimental-strip-types ./scripts/images-cleanup.ts 28 | timeout-minutes: 30 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/report.template.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | baseDir: 5 | type: string 6 | required: true 7 | scenarioName: 8 | type: string 9 | required: true 10 | scenarioDir: 11 | type: string 12 | required: true 13 | 14 | jobs: 15 | report: 16 | runs-on: ubuntu-24.04 17 | name: report 18 | steps: 19 | - name: checkout 20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 21 | with: 22 | token: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | - name: download artifacts 25 | id: download 26 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 27 | with: 28 | path: artifacts 29 | pattern: federation* 30 | 31 | - name: setup node 32 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 33 | with: 34 | node-version: 21 35 | 36 | - name: install dependencies 37 | run: yarn install 38 | working-directory: ${{ inputs.baseDir }}/scenarios/${{ inputs.scenarioDir }} 39 | 40 | - name: generate report 41 | run: yarn generate-report ${{ steps.download.outputs.download-path }} 42 | working-directory: ${{ inputs.baseDir }}/scenarios/${{ inputs.scenarioDir }} 43 | env: 44 | SCENARIO_ARTIFACTS_PREFIX: ${{ inputs.baseDir }}_${{ inputs.scenarioName }}_ 45 | SCENARIO_TITLE: ${{ inputs.baseDir }}/${{ inputs.scenarioName }} 46 | CF_IMAGES_LINK: ${{ secrets.CF_IMAGES_LINK }} 47 | CF_IMAGES_TOKEN: ${{ secrets.CF_IMAGES_TOKEN }} 48 | 49 | - name: publish report (pull_request) 50 | uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2 51 | if: github.event_name == 'pull_request' 52 | with: 53 | header: ${{ inputs.scenarioName }} 54 | path: ${{ inputs.baseDir }}/scenarios/${{ inputs.scenarioDir }}/result.md 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.githubToken }} 57 | 58 | - name: update summary 59 | run: cat ${{ inputs.baseDir }}/scenarios/${{ inputs.scenarioDir }}/result.md >> $GITHUB_STEP_SUMMARY 60 | 61 | - name: copy report (workflow_dispatch/push) 62 | if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' 63 | run: | 64 | cp ${{ inputs.baseDir }}/scenarios/${{ inputs.scenarioDir }}/result.md ${{ inputs.baseDir }}/scenarios/${{ inputs.scenarioName }}/README.md 65 | 66 | - run: | 67 | git config user.name github-actions 68 | git config user.email github-actions@github.com 69 | 70 | - name: publish report (workflow_dispatch/push) 71 | uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9 72 | if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' 73 | with: 74 | add: ${{ inputs.baseDir }}/scenarios/${{ inputs.scenarioName }}/README.md 75 | message: "Update results for scenario: ${{ inputs.scenarioName }}" 76 | pull: "--rebase --autostash" 77 | author_email: github-actions@github.com 78 | author_name: github-actions 79 | github_token: ${{ secrets.GITHUB_TOKEN }} 80 | 81 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 82 | if: always() 83 | with: 84 | name: ${{ inputs.baseDir }}-${{ inputs.scenarioName }}-report 85 | path: | 86 | ${{ inputs.baseDir }}/scenarios/${{ inputs.scenarioDir }}/result.md 87 | ${{ inputs.baseDir }}/scenarios/${{ inputs.scenarioDir }}/report.vega.json 88 | ${{ inputs.baseDir }}/scenarios/${{ inputs.scenarioDir }}/report.svg 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | temp 2 | node_modules 3 | build 4 | dist 5 | out 6 | results 7 | k6_metrics.json 8 | k6_summary.json 9 | k6_summary.txt 10 | rps.json 11 | summary.md 12 | engine.bin 13 | *.png 14 | result.md 15 | gateway_log.txt 16 | .mesh 17 | target 18 | report.vega.json 19 | report.svg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 The Guild 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Gateways Benchmark 2 | 3 | This repository is a collection is different scenarios and tests performed on different implementations of GraphQL gateways. 4 | 5 | This goals of this repo: 6 | 7 | 1. **Provide a transparent, accurate and descriptive benchmark testing for different tools:** 8 | 9 | ➡️ All tests are running in Docker containers, with the same configuration. 10 | 11 | ➡️ During the test, tools like [cadvisor](https://github.com/google/cadvisor) are monitoring stats. 12 | 13 | ➡️ Promethues and Grafana are stores results and stats for every run, and we export and embed it as PNG. 14 | 15 | ➡️ Every scenario have a different report, based on what it measures. 16 | 17 | ➡️ We are using a dedicated GitHub Actions runners, with `concurrency=1` to make sure tests are running as standalone. 18 | 19 | 2. **Always be up to date:** 20 | 21 | ➡️ This allows tools to improve over time. 22 | 23 | ➡️ All code in the repository is open and can be changed by the community. 24 | 25 | ➡️ Scenarios are executed for every PR, and reports are generated automatically per scenario. 26 | 27 | ➡️ [Renovate](https://github.com/renovatebot/renovate) keeps all dependencies up-to-date. 28 | 29 | 3. **Various scenarios** 30 | 31 | ➡️ We are trying to create real-life scenarios based on [our experience and our customers](https://the-guild.dev). 32 | 33 | ➡️ Each scenario has differnt setup and measure different stats. 34 | 35 | # Scenarios 36 | 37 | ## `fed-constant-vus-over-time` 38 | 39 | [Latest Results](./federation/scenarios/constant-vus-over-time/README.md) 40 | 41 | This scenario runs the following: 42 | 43 | 1. 4 GraphQL subgraphs in dedicated services 44 | 2. A GraphQL gateway compatible with the Apollo Federation spec 45 | 3. Constant rate of VUs over fixed time span 46 | 47 | This measures the following: 48 | 49 | 1. RPS (requests per second) rate 50 | 2. Request duration (average, p95) 51 | 3. Request failures (count) 52 | 4. CPU usage during the entire execution 53 | 5. RAM usage during the entire execution 54 | 6. HTTP layer timings 55 | 56 | > This scenario uses Federation spec with all gateways that supports this kind of specification (not all gateways supports v2 spec). 57 | 58 | ## `fed-constant-vus-subgraphs-delay` 59 | 60 | [Latest Results](./federation/scenarios/constant-vus-subgraphs-delay/README.md) 61 | 62 | This scenario runs the same flow as `fed-constant-vus-over-time` but with an intentional delay on each upstream Subgraph. This creates more stress and increases memory in the server due to the more inflight requests. 63 | 64 | ## `fed-constant-vus-subgraphs-delay-resources` 65 | 66 | [Latest Results](./federation/scenarios/constant-vus-subgraphs-delay-resources/README.md) 67 | 68 | This scenario runs the same flow as `fed-constant-vus-subgraphs-delay` but with more resources (CPU and RAM) allocated for the gateway. 69 | 70 | ## `fed-ramping-vus` 71 | 72 | [Latest Results](./federation/scenarios/ramping-vus/README.md) 73 | 74 | This scenario runs the following: 75 | 76 | 1. 4 GraphQL subgraphs in dedicated services 77 | 2. A GraphQL gateway compatible with the Apollo Federation spec 78 | 3. Gradually ramping VUs to a high number, to demo a stress scenario 79 | 80 | This measures the following: 81 | 1. RPS (requests per second) rate 82 | 2. Request duration (average, med, max, p95) 83 | 3. Request failures (count) 84 | 4. CPU usage during the entire execution 85 | 5. RAM usage during the entire execution 86 | 6. HTTP layer timings 87 | 88 | > This scenario uses Federation spec with all gateways that supports this kind of specification (not all gateways supports v2 spec). 89 | -------------------------------------------------------------------------------- /docker-compose.metrics.yaml: -------------------------------------------------------------------------------- 1 | networks: 2 | test: 3 | name: test 4 | 5 | services: 6 | prometheus: 7 | image: prom/prometheus:v3.4.0 8 | command: 9 | - --web.enable-remote-write-receiver 10 | - --enable-feature=native-histograms 11 | - --config.file=/etc/prometheus/prometheus.yaml 12 | networks: 13 | - test 14 | ports: 15 | - "9090:9090" 16 | logging: 17 | driver: none 18 | volumes: 19 | - ./prometheus:/etc/prometheus/ 20 | 21 | grafana: 22 | image: grafana/grafana:12.0.1 23 | networks: 24 | - test 25 | ports: 26 | - "3000:3000" 27 | environment: 28 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin 29 | - GF_AUTH_ANONYMOUS_ENABLED=true 30 | - GF_AUTH_BASIC_ENABLED=false 31 | volumes: 32 | - ./grafana:/etc/grafana/provisioning/ 33 | logging: 34 | driver: none 35 | 36 | cadvisor: 37 | image: gcr.io/cadvisor/cadvisor:v0.52.1 38 | ports: 39 | - 8080:8080 40 | volumes: 41 | - /var/run:/var/run:ro 42 | - /sys:/sys:ro 43 | - /var/lib/docker/:/var/lib/docker:ro 44 | - /var/run/docker.sock:/var/run/docker.sock:ro 45 | - /etc/machine-id:/etc/machine-id:ro 46 | - /var/lib/dbus/machine-id:/var/lib/dbus/machine-id:ro 47 | networks: 48 | - test 49 | restart: unless-stopped 50 | container_name: cadvisor 51 | command: --enable_metrics=cpu,cpuLoad,sched,process,resctrl,memory,cpuset,advtcp,network,tcp,perf_event 52 | privileged: true 53 | devices: 54 | - /dev/kmsg:/dev/kmsg 55 | logging: 56 | driver: none -------------------------------------------------------------------------------- /federation/gateways/apollo-router/config.yaml: -------------------------------------------------------------------------------- 1 | supergraph: 2 | path: /graphql 3 | listen: 0.0.0.0:4000 4 | traffic_shaping: 5 | all: 6 | deduplicate_query: true 7 | health_check: 8 | listen: 127.0.0.1:8088 9 | enabled: true 10 | path: /health -------------------------------------------------------------------------------- /federation/gateways/apollo-router/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | gateway: 3 | image: ghcr.io/apollographql/router:v2.2.1 4 | container_name: gateway 5 | networks: 6 | - test 7 | ports: 8 | - "0.0.0.0:4000:4000" 9 | environment: 10 | APOLLO_ROUTER_LISTEN_ADDRESS: "0.0.0.0:4000" 11 | depends_on: 12 | accounts: 13 | condition: service_healthy 14 | inventory: 15 | condition: service_healthy 16 | products: 17 | condition: service_healthy 18 | reviews: 19 | condition: service_healthy 20 | volumes: 21 | - type: bind 22 | source: federation/gateways/apollo-router/supergraph.graphql 23 | target: /dist/schema/local.graphql 24 | - type: bind 25 | source: federation/gateways/apollo-router/config.yaml 26 | target: /dist/config/router.yaml 27 | healthcheck: 28 | test: 29 | [ 30 | "CMD", 31 | "/usr/lib/apt/apt-helper", 32 | "download-file", 33 | "http://127.0.0.1:8088/health?ready", 34 | "/tmp/health", 35 | ] 36 | interval: 3s 37 | timeout: 5s 38 | retries: 10 39 | command: ["-c", "config/router.yaml", "-s", "schema/local.graphql"] 40 | deploy: 41 | resources: 42 | limits: 43 | cpus: ${CPU_LIMIT:-1} 44 | memory: ${MEM_LIMIT:-1gb} 45 | networks: 46 | test: 47 | name: test 48 | -------------------------------------------------------------------------------- /federation/gateways/apollo-router/supergraph.graphql: -------------------------------------------------------------------------------- 1 | schema 2 | @link(url: "https://specs.apollo.dev/link/v1.0") 3 | @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) 4 | { 5 | query: Query 6 | } 7 | 8 | directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE 9 | 10 | directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION 11 | 12 | directive @join__graph(name: String!, url: String!) on ENUM_VALUE 13 | 14 | directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE 15 | 16 | directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR 17 | 18 | directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION 19 | 20 | directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA 21 | 22 | scalar join__FieldSet 23 | 24 | enum join__Graph { 25 | ACCOUNTS @join__graph(name: "accounts", url: "http://accounts:4001/graphql") 26 | INVENTORY @join__graph(name: "inventory", url: "http://inventory:4002/graphql") 27 | PRODUCTS @join__graph(name: "products", url: "http://products:4003/graphql") 28 | REVIEWS @join__graph(name: "reviews", url: "http://reviews:4004/graphql") 29 | } 30 | 31 | scalar link__Import 32 | 33 | enum link__Purpose { 34 | """ 35 | `SECURITY` features provide metadata necessary to securely resolve fields. 36 | """ 37 | SECURITY 38 | 39 | """ 40 | `EXECUTION` features provide metadata necessary for operation execution. 41 | """ 42 | EXECUTION 43 | } 44 | 45 | type Product 46 | @join__type(graph: INVENTORY, key: "upc") 47 | @join__type(graph: PRODUCTS, key: "upc") 48 | @join__type(graph: REVIEWS, key: "upc") 49 | { 50 | upc: String! 51 | weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) 52 | price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) 53 | inStock: Boolean @join__field(graph: INVENTORY) 54 | shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") 55 | name: String @join__field(graph: PRODUCTS) 56 | reviews: [Review] @join__field(graph: REVIEWS) 57 | } 58 | 59 | type Query 60 | @join__type(graph: ACCOUNTS) 61 | @join__type(graph: INVENTORY) 62 | @join__type(graph: PRODUCTS) 63 | @join__type(graph: REVIEWS) 64 | { 65 | me: User @join__field(graph: ACCOUNTS) 66 | user(id: ID!): User @join__field(graph: ACCOUNTS) 67 | users: [User] @join__field(graph: ACCOUNTS) 68 | topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) 69 | } 70 | 71 | type Review 72 | @join__type(graph: REVIEWS, key: "id") 73 | { 74 | id: ID! 75 | body: String 76 | product: Product 77 | author: User @join__field(graph: REVIEWS, provides: "username") 78 | } 79 | 80 | type User 81 | @join__type(graph: ACCOUNTS, key: "id") 82 | @join__type(graph: REVIEWS, key: "id") 83 | { 84 | id: ID! 85 | name: String @join__field(graph: ACCOUNTS) 86 | username: String @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS, external: true) 87 | birthday: Int @join__field(graph: ACCOUNTS) 88 | reviews: [Review] @join__field(graph: REVIEWS) 89 | } -------------------------------------------------------------------------------- /federation/gateways/apollo-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:23-slim 2 | 3 | RUN apt-get update -y && apt-get install curl -y 4 | 5 | WORKDIR /home/gw 6 | COPY supergraph.graphql index.ts ./ 7 | COPY package.json yarn.lock ./ 8 | 9 | RUN yarn install --production 10 | ENV PORT=4000 11 | ENV NODE_ENV=production 12 | 13 | EXPOSE 4000 14 | 15 | CMD ["yarn", "start"] 16 | -------------------------------------------------------------------------------- /federation/gateways/apollo-server/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | gateway: 3 | image: gateway/apollo-server 4 | container_name: gateway 5 | networks: 6 | - test 7 | ports: 8 | - 4000:4000 9 | environment: 10 | PORT: 4000 11 | build: 12 | context: ${BASE_DIR:-.}/../../gateways/apollo-server 13 | dockerfile: ./Dockerfile 14 | depends_on: 15 | accounts: 16 | condition: service_healthy 17 | inventory: 18 | condition: service_healthy 19 | products: 20 | condition: service_healthy 21 | reviews: 22 | condition: service_healthy 23 | healthcheck: 24 | test: ["CMD", "curl", "-f", "-X", "POST", "-H" ,"Content-type: application/json", "http://localhost:4000/graphql", "--data-raw", "{\"query\": \"query { __typename }\"}"] 25 | interval: 3s 26 | timeout: 5s 27 | retries: 10 28 | deploy: 29 | resources: 30 | limits: 31 | cpus: ${CPU_LIMIT:-1} 32 | memory: ${MEM_LIMIT:-1gb} 33 | networks: 34 | test: 35 | name: test -------------------------------------------------------------------------------- /federation/gateways/apollo-server/index.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from '@apollo/server'; 2 | import { ApolloGateway } from '@apollo/gateway'; 3 | import { startStandaloneServer } from '@apollo/server/standalone'; 4 | import { readFileSync } from 'fs'; 5 | 6 | async function main() { 7 | const supergraphSdl = readFileSync('./supergraph.graphql').toString(); 8 | const gateway = new ApolloGateway({ supergraphSdl }); 9 | const server = new ApolloServer({ gateway }); 10 | 11 | const { url } = await startStandaloneServer(server, { 12 | listen: { port: process.env.PORT ? parseInt(process.env.PORT) : 4000 }, 13 | }); 14 | 15 | console.log(`🚀 Server ready at ${url}`); 16 | } 17 | 18 | main().catch(console.error) -------------------------------------------------------------------------------- /federation/gateways/apollo-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-gateway-js", 3 | "scripts": { 4 | "start": "tsx index.ts" 5 | }, 6 | "dependencies": { 7 | "tsx": "4.19.4", 8 | "typescript": "5.8.3", 9 | "graphql": "16.11.0", 10 | "@apollo/server": "4.12.1", 11 | "@apollo/gateway": "2.10.2", 12 | "@apollo/composition": "2.10.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /federation/gateways/apollo-server/supergraph.graphql: -------------------------------------------------------------------------------- 1 | schema 2 | @link(url: "https://specs.apollo.dev/link/v1.0") 3 | @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) 4 | { 5 | query: Query 6 | } 7 | 8 | directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE 9 | 10 | directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION 11 | 12 | directive @join__graph(name: String!, url: String!) on ENUM_VALUE 13 | 14 | directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE 15 | 16 | directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR 17 | 18 | directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION 19 | 20 | directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA 21 | 22 | scalar join__FieldSet 23 | 24 | enum join__Graph { 25 | ACCOUNTS @join__graph(name: "accounts", url: "http://accounts:4001/graphql") 26 | INVENTORY @join__graph(name: "inventory", url: "http://inventory:4002/graphql") 27 | PRODUCTS @join__graph(name: "products", url: "http://products:4003/graphql") 28 | REVIEWS @join__graph(name: "reviews", url: "http://reviews:4004/graphql") 29 | } 30 | 31 | scalar link__Import 32 | 33 | enum link__Purpose { 34 | """ 35 | `SECURITY` features provide metadata necessary to securely resolve fields. 36 | """ 37 | SECURITY 38 | 39 | """ 40 | `EXECUTION` features provide metadata necessary for operation execution. 41 | """ 42 | EXECUTION 43 | } 44 | 45 | type Product 46 | @join__type(graph: INVENTORY, key: "upc") 47 | @join__type(graph: PRODUCTS, key: "upc") 48 | @join__type(graph: REVIEWS, key: "upc") 49 | { 50 | upc: String! 51 | weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) 52 | price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) 53 | inStock: Boolean @join__field(graph: INVENTORY) 54 | shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") 55 | name: String @join__field(graph: PRODUCTS) 56 | reviews: [Review] @join__field(graph: REVIEWS) 57 | } 58 | 59 | type Query 60 | @join__type(graph: ACCOUNTS) 61 | @join__type(graph: INVENTORY) 62 | @join__type(graph: PRODUCTS) 63 | @join__type(graph: REVIEWS) 64 | { 65 | me: User @join__field(graph: ACCOUNTS) 66 | user(id: ID!): User @join__field(graph: ACCOUNTS) 67 | users: [User] @join__field(graph: ACCOUNTS) 68 | topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) 69 | } 70 | 71 | type Review 72 | @join__type(graph: REVIEWS, key: "id") 73 | { 74 | id: ID! 75 | body: String 76 | product: Product 77 | author: User @join__field(graph: REVIEWS, provides: "username") 78 | } 79 | 80 | type User 81 | @join__type(graph: ACCOUNTS, key: "id") 82 | @join__type(graph: REVIEWS, key: "id") 83 | { 84 | id: ID! 85 | name: String @join__field(graph: ACCOUNTS) 86 | username: String @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS, external: true) 87 | birthday: Int @join__field(graph: ACCOUNTS) 88 | reviews: [Review] @join__field(graph: REVIEWS) 89 | } -------------------------------------------------------------------------------- /federation/gateways/cosmo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:12-slim 2 | 3 | RUN apt-get update -y && apt-get install curl -y 4 | 5 | WORKDIR /home/gw 6 | COPY router.json ./ 7 | 8 | ENV ROUTER_VERSION=0.199.1 9 | 10 | RUN curl -L https://github.com/wundergraph/cosmo/releases/download/router%40${ROUTER_VERSION}/router-router@${ROUTER_VERSION}-linux-$(uname -m | sed s:aarch:arm:| sed s:x86_:amd:).tar.gz | gunzip -dc | tar x && mv router /usr/local/bin 11 | 12 | ENV LOG_LEVEL=fatal 13 | ENV LISTEN_ADDR=0.0.0.0:4000 14 | ENV TRACING_ENABLED=false 15 | ENV METRICS_ENABLED=false 16 | ENV METRICS_OTLP_ENABLED=false 17 | ENV GRAPHQL_METRICS_ENABLED=false 18 | ENV PROMETHEUS_ENABLED=false 19 | ENV ROUTER_CONFIG_PATH=router.json 20 | 21 | EXPOSE 4000 22 | 23 | WORKDIR /home/gw 24 | 25 | ENTRYPOINT ["/usr/local/bin/router"] 26 | -------------------------------------------------------------------------------- /federation/gateways/cosmo/accounts.graphql: -------------------------------------------------------------------------------- 1 | extend type Query { 2 | me: User 3 | user(id: ID!): User 4 | users: [User] 5 | } 6 | 7 | type User @key(fields: "id") { 8 | id: ID! 9 | name: String 10 | username: String 11 | birthday: Int 12 | } 13 | 14 | directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 15 | 16 | directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 17 | -------------------------------------------------------------------------------- /federation/gateways/cosmo/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | gateway: 3 | image: gateway/cosmo 4 | container_name: gateway 5 | networks: 6 | - test 7 | ports: 8 | - "0.0.0.0:4000:4000" 9 | build: 10 | context: ${BASE_DIR:-.}/../../gateways/cosmo 11 | dockerfile: ./Dockerfile 12 | depends_on: 13 | accounts: 14 | condition: service_healthy 15 | inventory: 16 | condition: service_healthy 17 | products: 18 | condition: service_healthy 19 | reviews: 20 | condition: service_healthy 21 | healthcheck: 22 | test: 23 | ["CMD", "curl", "-f", "-X", "GET", "http://localhost:4000/health/live"] 24 | interval: 8s 25 | timeout: 5s 26 | retries: 5 27 | deploy: 28 | resources: 29 | limits: 30 | cpus: ${CPU_LIMIT:-1} 31 | memory: ${MEM_LIMIT:-1gb} 32 | networks: 33 | test: 34 | name: test 35 | -------------------------------------------------------------------------------- /federation/gateways/cosmo/graph.yaml: -------------------------------------------------------------------------------- 1 | 2 | version: 1 3 | subgraphs: 4 | - name: accounts 5 | routing_url: http://accounts:4001/graphql 6 | schema: 7 | file: ./accounts.graphql 8 | - name: inventory 9 | routing_url: http://inventory:4002/graphql 10 | schema: 11 | file: ./inventory.graphql 12 | - name: products 13 | routing_url: http://products:4003/graphql 14 | schema: 15 | file: ./products.graphql 16 | - name: reviews 17 | routing_url: http://reviews:4004/graphql 18 | schema: 19 | file: ./reviews.graphql 20 | 21 | -------------------------------------------------------------------------------- /federation/gateways/cosmo/inventory.graphql: -------------------------------------------------------------------------------- 1 | extend type Product @key(fields: "upc") { 2 | upc: String! @external 3 | weight: Int @external 4 | price: Int @external 5 | inStock: Boolean 6 | shippingEstimate: Int @requires(fields: "price weight") 7 | } 8 | 9 | directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 10 | 11 | directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 12 | -------------------------------------------------------------------------------- /federation/gateways/cosmo/products.graphql: -------------------------------------------------------------------------------- 1 | type Product @key(fields: "upc") { 2 | upc: String! 3 | name: String 4 | price: Int 5 | weight: Int 6 | } 7 | 8 | extend type Query { 9 | topProducts(first: Int = 5): [Product] 10 | } 11 | 12 | directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 13 | 14 | directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 15 | -------------------------------------------------------------------------------- /federation/gateways/cosmo/reviews.graphql: -------------------------------------------------------------------------------- 1 | extend type Product @key(fields: "upc") { 2 | upc: String! @external 3 | reviews: [Review] 4 | } 5 | 6 | type Review @key(fields: "id") { 7 | id: ID! 8 | body: String 9 | product: Product 10 | author: User @provides(fields: "username") 11 | } 12 | 13 | extend type User @key(fields: "id") { 14 | id: ID! @external 15 | username: String @external 16 | reviews: [Review] 17 | } 18 | 19 | directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 20 | 21 | directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 22 | -------------------------------------------------------------------------------- /federation/gateways/cosmo/router.json: -------------------------------------------------------------------------------- 1 | { 2 | "engineConfig": { 3 | "defaultFlushInterval": "500", 4 | "datasourceConfigurations": [ 5 | { 6 | "kind": "GRAPHQL", 7 | "rootNodes": [ 8 | { "typeName": "Query", "fieldNames": ["me", "user", "users"] }, 9 | { 10 | "typeName": "User", 11 | "fieldNames": ["id", "name", "username", "birthday"] 12 | } 13 | ], 14 | "overrideFieldPathFromAlias": true, 15 | "customGraphql": { 16 | "fetch": { 17 | "url": { "staticVariableContent": "http://accounts:4001/graphql" }, 18 | "method": "POST", 19 | "body": {}, 20 | "baseUrl": {}, 21 | "path": {} 22 | }, 23 | "subscription": { 24 | "enabled": true, 25 | "url": { "staticVariableContent": "http://accounts:4001/graphql" }, 26 | "protocol": "GRAPHQL_SUBSCRIPTION_PROTOCOL_WS", 27 | "websocketSubprotocol": "GRAPHQL_WEBSOCKET_SUBPROTOCOL_AUTO" 28 | }, 29 | "federation": { 30 | "enabled": true, 31 | "serviceSdl": "extend type Query {\n me: User\n user(id: ID!): User\n users: [User]\n}\n\ntype User @key(fields: \"id\") {\n id: ID!\n name: String\n username: String\n birthday: Int\n}\n\ndirective @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n\ndirective @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n" 32 | }, 33 | "upstreamSchema": { 34 | "key": "db37a1abd369d6fee0ca43d7134e3998f9feba01" 35 | } 36 | }, 37 | "requestTimeoutSeconds": "10", 38 | "id": "0", 39 | "keys": [{ "typeName": "User", "selectionSet": "id" }] 40 | }, 41 | { 42 | "kind": "GRAPHQL", 43 | "rootNodes": [ 44 | { 45 | "typeName": "Product", 46 | "fieldNames": ["inStock", "shippingEstimate", "upc"], 47 | "externalFieldNames": ["upc", "weight", "price"] 48 | } 49 | ], 50 | "overrideFieldPathFromAlias": true, 51 | "customGraphql": { 52 | "fetch": { 53 | "url": { "staticVariableContent": "http://inventory:4002/graphql" }, 54 | "method": "POST", 55 | "body": {}, 56 | "baseUrl": {}, 57 | "path": {} 58 | }, 59 | "subscription": { 60 | "enabled": true, 61 | "url": { "staticVariableContent": "http://inventory:4002/graphql" }, 62 | "protocol": "GRAPHQL_SUBSCRIPTION_PROTOCOL_WS", 63 | "websocketSubprotocol": "GRAPHQL_WEBSOCKET_SUBPROTOCOL_AUTO" 64 | }, 65 | "federation": { 66 | "enabled": true, 67 | "serviceSdl": "extend type Product @key(fields: \"upc\") {\n upc: String! @external\n weight: Int @external\n price: Int @external\n inStock: Boolean\n shippingEstimate: Int @requires(fields: \"price weight\")\n}\n\ndirective @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n\ndirective @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n" 68 | }, 69 | "upstreamSchema": { 70 | "key": "64a29f293bb6d35526ce51e1715b8032cdfb31bf" 71 | } 72 | }, 73 | "requestTimeoutSeconds": "10", 74 | "id": "1", 75 | "keys": [{ "typeName": "Product", "selectionSet": "upc" }], 76 | "requires": [ 77 | { 78 | "typeName": "Product", 79 | "fieldName": "shippingEstimate", 80 | "selectionSet": "price weight" 81 | } 82 | ] 83 | }, 84 | { 85 | "kind": "GRAPHQL", 86 | "rootNodes": [ 87 | { 88 | "typeName": "Product", 89 | "fieldNames": ["upc", "name", "price", "weight"] 90 | }, 91 | { "typeName": "Query", "fieldNames": ["topProducts"] } 92 | ], 93 | "overrideFieldPathFromAlias": true, 94 | "customGraphql": { 95 | "fetch": { 96 | "url": { "staticVariableContent": "http://products:4003/graphql" }, 97 | "method": "POST", 98 | "body": {}, 99 | "baseUrl": {}, 100 | "path": {} 101 | }, 102 | "subscription": { 103 | "enabled": true, 104 | "url": { "staticVariableContent": "http://products:4003/graphql" }, 105 | "protocol": "GRAPHQL_SUBSCRIPTION_PROTOCOL_WS", 106 | "websocketSubprotocol": "GRAPHQL_WEBSOCKET_SUBPROTOCOL_AUTO" 107 | }, 108 | "federation": { 109 | "enabled": true, 110 | "serviceSdl": "type Product @key(fields: \"upc\") {\n upc: String!\n name: String\n price: Int\n weight: Int\n}\n\nextend type Query {\n topProducts(first: Int = 5): [Product]\n}\n\ndirective @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n\ndirective @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n" 111 | }, 112 | "upstreamSchema": { 113 | "key": "1afef6ee5330df98b15899d48b7d738c0f43f429" 114 | } 115 | }, 116 | "requestTimeoutSeconds": "10", 117 | "id": "2", 118 | "keys": [{ "typeName": "Product", "selectionSet": "upc" }] 119 | }, 120 | { 121 | "kind": "GRAPHQL", 122 | "rootNodes": [ 123 | { 124 | "typeName": "Product", 125 | "fieldNames": ["reviews", "upc"], 126 | "externalFieldNames": ["upc"] 127 | }, 128 | { 129 | "typeName": "Review", 130 | "fieldNames": ["id", "body", "product", "author"] 131 | }, 132 | { 133 | "typeName": "User", 134 | "fieldNames": ["reviews", "id"], 135 | "externalFieldNames": ["id", "username"] 136 | } 137 | ], 138 | "overrideFieldPathFromAlias": true, 139 | "customGraphql": { 140 | "fetch": { 141 | "url": { "staticVariableContent": "http://reviews:4004/graphql" }, 142 | "method": "POST", 143 | "body": {}, 144 | "baseUrl": {}, 145 | "path": {} 146 | }, 147 | "subscription": { 148 | "enabled": true, 149 | "url": { "staticVariableContent": "http://reviews:4004/graphql" }, 150 | "protocol": "GRAPHQL_SUBSCRIPTION_PROTOCOL_WS", 151 | "websocketSubprotocol": "GRAPHQL_WEBSOCKET_SUBPROTOCOL_AUTO" 152 | }, 153 | "federation": { 154 | "enabled": true, 155 | "serviceSdl": "extend type Product @key(fields: \"upc\") {\n upc: String! @external\n reviews: [Review]\n}\n\ntype Review @key(fields: \"id\") {\n id: ID!\n body: String\n product: Product\n author: User @provides(fields: \"username\")\n}\n\nextend type User @key(fields: \"id\") {\n id: ID! @external\n username: String @external\n reviews: [Review]\n}\n\ndirective @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n\ndirective @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n" 156 | }, 157 | "upstreamSchema": { 158 | "key": "1b9a4a72484d26b758d37d3884812af6affbc02f" 159 | } 160 | }, 161 | "requestTimeoutSeconds": "10", 162 | "id": "3", 163 | "keys": [ 164 | { "typeName": "Product", "selectionSet": "upc" }, 165 | { "typeName": "Review", "selectionSet": "id" }, 166 | { "typeName": "User", "selectionSet": "id" } 167 | ], 168 | "provides": [ 169 | { 170 | "typeName": "Review", 171 | "fieldName": "author", 172 | "selectionSet": "username" 173 | } 174 | ] 175 | } 176 | ], 177 | "fieldConfigurations": [ 178 | { 179 | "typeName": "Query", 180 | "fieldName": "user", 181 | "argumentsConfiguration": [ 182 | { "name": "id", "sourceType": "FIELD_ARGUMENT" } 183 | ] 184 | }, 185 | { 186 | "typeName": "Query", 187 | "fieldName": "topProducts", 188 | "argumentsConfiguration": [ 189 | { "name": "first", "sourceType": "FIELD_ARGUMENT" } 190 | ] 191 | } 192 | ], 193 | "graphqlSchema": "schema {\n query: Query\n}\n\ndirective @tag(name: String!) repeatable on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION\n\ntype Query {\n me: User\n user(id: ID!): User\n users: [User]\n topProducts(first: Int = 5): [Product]\n}\n\ntype User {\n id: ID!\n name: String\n username: String\n birthday: Int\n reviews: [Review]\n}\n\ntype Product {\n upc: String!\n weight: Int\n price: Int\n inStock: Boolean\n shippingEstimate: Int\n name: String\n reviews: [Review]\n}\n\ntype Review {\n id: ID!\n body: String\n product: Product\n author: User\n}", 194 | "stringStorage": { 195 | "db37a1abd369d6fee0ca43d7134e3998f9feba01": "schema {\n query: Query\n}\n\ndirective @extends on INTERFACE | OBJECT\n\ndirective @external on FIELD_DEFINITION | OBJECT\n\ndirective @key(fields: openfed__FieldSet!, resolvable: Boolean = true) repeatable on INTERFACE | OBJECT\n\ndirective @provides(fields: openfed__FieldSet!) on FIELD_DEFINITION\n\ndirective @requires(fields: openfed__FieldSet!) on FIELD_DEFINITION\n\ndirective @tag(name: String!) repeatable on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION\n\ntype Query {\n me: User\n user(id: ID!): User\n users: [User]\n}\n\ntype User @key(fields: \"id\") {\n birthday: Int\n id: ID!\n name: String\n username: String\n}\n\nscalar openfed__FieldSet", 196 | "64a29f293bb6d35526ce51e1715b8032cdfb31bf": "directive @extends on INTERFACE | OBJECT\n\ndirective @external on FIELD_DEFINITION | OBJECT\n\ndirective @key(fields: openfed__FieldSet!, resolvable: Boolean = true) repeatable on INTERFACE | OBJECT\n\ndirective @provides(fields: openfed__FieldSet!) on FIELD_DEFINITION\n\ndirective @requires(fields: openfed__FieldSet!) on FIELD_DEFINITION\n\ndirective @tag(name: String!) repeatable on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION\n\ntype Product @key(fields: \"upc\") {\n inStock: Boolean\n price: Int @external\n shippingEstimate: Int @requires(fields: \"price weight\")\n upc: String! @external\n weight: Int @external\n}\n\nscalar openfed__FieldSet", 197 | "1afef6ee5330df98b15899d48b7d738c0f43f429": "schema {\n query: Query\n}\n\ndirective @extends on INTERFACE | OBJECT\n\ndirective @external on FIELD_DEFINITION | OBJECT\n\ndirective @key(fields: openfed__FieldSet!, resolvable: Boolean = true) repeatable on INTERFACE | OBJECT\n\ndirective @provides(fields: openfed__FieldSet!) on FIELD_DEFINITION\n\ndirective @requires(fields: openfed__FieldSet!) on FIELD_DEFINITION\n\ndirective @tag(name: String!) repeatable on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION\n\ntype Product @key(fields: \"upc\") {\n name: String\n price: Int\n upc: String!\n weight: Int\n}\n\ntype Query {\n topProducts(first: Int = 5): [Product]\n}\n\nscalar openfed__FieldSet", 198 | "1b9a4a72484d26b758d37d3884812af6affbc02f": "directive @extends on INTERFACE | OBJECT\n\ndirective @external on FIELD_DEFINITION | OBJECT\n\ndirective @key(fields: openfed__FieldSet!, resolvable: Boolean = true) repeatable on INTERFACE | OBJECT\n\ndirective @provides(fields: openfed__FieldSet!) on FIELD_DEFINITION\n\ndirective @requires(fields: openfed__FieldSet!) on FIELD_DEFINITION\n\ndirective @tag(name: String!) repeatable on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION\n\ntype Product @key(fields: \"upc\") {\n reviews: [Review]\n upc: String! @external\n}\n\ntype Review @key(fields: \"id\") {\n author: User @provides(fields: \"username\")\n body: String\n id: ID!\n product: Product\n}\n\ntype User @key(fields: \"id\") {\n id: ID! @external\n reviews: [Review]\n username: String @external\n}\n\nscalar openfed__FieldSet" 199 | } 200 | }, 201 | "version": "6e9dbed6-ec25-44f1-8693-4d44274fe9a6", 202 | "subgraphs": [ 203 | { 204 | "id": "0", 205 | "name": "accounts", 206 | "routingUrl": "http://accounts:4001/graphql" 207 | }, 208 | { 209 | "id": "1", 210 | "name": "inventory", 211 | "routingUrl": "http://inventory:4002/graphql" 212 | }, 213 | { 214 | "id": "2", 215 | "name": "products", 216 | "routingUrl": "http://products:4003/graphql" 217 | }, 218 | { 219 | "id": "3", 220 | "name": "reviews", 221 | "routingUrl": "http://reviews:4004/graphql" 222 | } 223 | ] 224 | } 225 | -------------------------------------------------------------------------------- /federation/gateways/grafbase/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/grafbase/gateway:0.39.0 2 | 3 | COPY supergraph.graphql ./ 4 | 5 | EXPOSE 4000 6 | 7 | CMD ["--schema", "supergraph.graphql", "--listen-address", "0.0.0.0:4000"] 8 | -------------------------------------------------------------------------------- /federation/gateways/grafbase/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | gateway: 3 | image: gateway/grafbase 4 | container_name: gateway 5 | build: 6 | context: ${BASE_DIR:-.}/../../gateways/grafbase 7 | dockerfile: ./Dockerfile 8 | networks: 9 | - test 10 | ports: 11 | - "0.0.0.0:4000:4000" 12 | depends_on: 13 | accounts: 14 | condition: service_healthy 15 | inventory: 16 | condition: service_healthy 17 | products: 18 | condition: service_healthy 19 | reviews: 20 | condition: service_healthy 21 | healthcheck: 22 | test: ["CMD", "curl", "-f", "-X", "GET", "http://localhost:4000/health"] 23 | interval: 3s 24 | timeout: 5s 25 | retries: 10 26 | deploy: 27 | resources: 28 | limits: 29 | cpus: ${CPU_LIMIT:-1} 30 | memory: ${MEM_LIMIT:-1gb} 31 | networks: 32 | test: 33 | name: test 34 | -------------------------------------------------------------------------------- /federation/gateways/grafbase/supergraph.graphql: -------------------------------------------------------------------------------- 1 | schema 2 | @link(url: "https://specs.apollo.dev/link/v1.0") 3 | @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) 4 | { 5 | query: Query 6 | } 7 | 8 | directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE 9 | 10 | directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION 11 | 12 | directive @join__graph(name: String!, url: String!) on ENUM_VALUE 13 | 14 | directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE 15 | 16 | directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR 17 | 18 | directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION 19 | 20 | directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA 21 | 22 | scalar join__FieldSet 23 | 24 | enum join__Graph { 25 | ACCOUNTS @join__graph(name: "accounts", url: "http://accounts:4001/graphql") 26 | INVENTORY @join__graph(name: "inventory", url: "http://inventory:4002/graphql") 27 | PRODUCTS @join__graph(name: "products", url: "http://products:4003/graphql") 28 | REVIEWS @join__graph(name: "reviews", url: "http://reviews:4004/graphql") 29 | } 30 | 31 | scalar link__Import 32 | 33 | enum link__Purpose { 34 | """ 35 | `SECURITY` features provide metadata necessary to securely resolve fields. 36 | """ 37 | SECURITY 38 | 39 | """ 40 | `EXECUTION` features provide metadata necessary for operation execution. 41 | """ 42 | EXECUTION 43 | } 44 | 45 | type Product 46 | @join__type(graph: INVENTORY, key: "upc") 47 | @join__type(graph: PRODUCTS, key: "upc") 48 | @join__type(graph: REVIEWS, key: "upc") 49 | { 50 | upc: String! 51 | weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) 52 | price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) 53 | inStock: Boolean @join__field(graph: INVENTORY) 54 | shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") 55 | name: String @join__field(graph: PRODUCTS) 56 | reviews: [Review] @join__field(graph: REVIEWS) 57 | } 58 | 59 | type Query 60 | @join__type(graph: ACCOUNTS) 61 | @join__type(graph: INVENTORY) 62 | @join__type(graph: PRODUCTS) 63 | @join__type(graph: REVIEWS) 64 | { 65 | me: User @join__field(graph: ACCOUNTS) 66 | user(id: ID!): User @join__field(graph: ACCOUNTS) 67 | users: [User] @join__field(graph: ACCOUNTS) 68 | topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) 69 | } 70 | 71 | type Review 72 | @join__type(graph: REVIEWS, key: "id") 73 | { 74 | id: ID! 75 | body: String 76 | product: Product 77 | author: User @join__field(graph: REVIEWS, provides: "username") 78 | } 79 | 80 | type User 81 | @join__type(graph: ACCOUNTS, key: "id") 82 | @join__type(graph: REVIEWS, key: "id") 83 | { 84 | id: ID! 85 | name: String @join__field(graph: ACCOUNTS) 86 | username: String @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS, external: true) 87 | birthday: Int @join__field(graph: ACCOUNTS) 88 | reviews: [Review] @join__field(graph: REVIEWS) 89 | } -------------------------------------------------------------------------------- /federation/gateways/hive-gateway-bun/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | gateway: 3 | image: ghcr.io/graphql-hive/gateway:1.14.2-bun 4 | container_name: gateway 5 | networks: 6 | - test 7 | ports: 8 | - "0.0.0.0:4000:4000" 9 | environment: 10 | - NODE_ENV=production 11 | - FORK=${FORK} 12 | - JIT=true 13 | depends_on: 14 | accounts: 15 | condition: service_healthy 16 | inventory: 17 | condition: service_healthy 18 | products: 19 | condition: service_healthy 20 | reviews: 21 | condition: service_healthy 22 | volumes: 23 | - type: bind 24 | source: federation/gateways/hive-gateway-bun/supergraph.graphql 25 | target: /serve/supergraph.graphql 26 | healthcheck: 27 | test: 28 | [ 29 | "CMD", 30 | "/usr/lib/apt/apt-helper", 31 | "download-file", 32 | "http://127.0.0.1:4000/graphql?query=%7B+users+%7B+reviews+%7B+product+%7B+reviews+%7B+author+%7B+reviews+%7B+product+%7B+name+%7D+%7D+%7D+%7D+%7D+%7D+%7D+%7D", 33 | "/tmp/health", 34 | ] 35 | interval: 3s 36 | timeout: 5s 37 | retries: 10 38 | command: ["supergraph", "--jit"] 39 | deploy: 40 | resources: 41 | limits: 42 | cpus: ${CPU_LIMIT:-1} 43 | memory: ${MEM_LIMIT:-1gb} 44 | networks: 45 | test: 46 | name: test 47 | -------------------------------------------------------------------------------- /federation/gateways/hive-gateway-bun/supergraph.graphql: -------------------------------------------------------------------------------- 1 | schema 2 | @link(url: "https://specs.apollo.dev/link/v1.0") 3 | @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) 4 | { 5 | query: Query 6 | } 7 | 8 | directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE 9 | 10 | directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION 11 | 12 | directive @join__graph(name: String!, url: String!) on ENUM_VALUE 13 | 14 | directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE 15 | 16 | directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR 17 | 18 | directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION 19 | 20 | directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA 21 | 22 | scalar join__FieldSet 23 | 24 | enum join__Graph { 25 | ACCOUNTS @join__graph(name: "accounts", url: "http://accounts:4001/graphql") 26 | INVENTORY @join__graph(name: "inventory", url: "http://inventory:4002/graphql") 27 | PRODUCTS @join__graph(name: "products", url: "http://products:4003/graphql") 28 | REVIEWS @join__graph(name: "reviews", url: "http://reviews:4004/graphql") 29 | } 30 | 31 | scalar link__Import 32 | 33 | enum link__Purpose { 34 | """ 35 | `SECURITY` features provide metadata necessary to securely resolve fields. 36 | """ 37 | SECURITY 38 | 39 | """ 40 | `EXECUTION` features provide metadata necessary for operation execution. 41 | """ 42 | EXECUTION 43 | } 44 | 45 | type Product 46 | @join__type(graph: INVENTORY, key: "upc") 47 | @join__type(graph: PRODUCTS, key: "upc") 48 | @join__type(graph: REVIEWS, key: "upc") 49 | { 50 | upc: String! 51 | weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) 52 | price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) 53 | inStock: Boolean @join__field(graph: INVENTORY) 54 | shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") 55 | name: String @join__field(graph: PRODUCTS) 56 | reviews: [Review] @join__field(graph: REVIEWS) 57 | } 58 | 59 | type Query 60 | @join__type(graph: ACCOUNTS) 61 | @join__type(graph: INVENTORY) 62 | @join__type(graph: PRODUCTS) 63 | @join__type(graph: REVIEWS) 64 | { 65 | me: User @join__field(graph: ACCOUNTS) 66 | user(id: ID!): User @join__field(graph: ACCOUNTS) 67 | users: [User] @join__field(graph: ACCOUNTS) 68 | topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) 69 | } 70 | 71 | type Review 72 | @join__type(graph: REVIEWS, key: "id") 73 | { 74 | id: ID! 75 | body: String 76 | product: Product 77 | author: User @join__field(graph: REVIEWS, provides: "username") 78 | } 79 | 80 | type User 81 | @join__type(graph: ACCOUNTS, key: "id") 82 | @join__type(graph: REVIEWS, key: "id") 83 | { 84 | id: ID! 85 | name: String @join__field(graph: ACCOUNTS) 86 | username: String @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS, external: true) 87 | birthday: Int @join__field(graph: ACCOUNTS) 88 | reviews: [Review] @join__field(graph: REVIEWS) 89 | } -------------------------------------------------------------------------------- /federation/gateways/hive-gateway/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | gateway: 3 | image: ghcr.io/graphql-hive/gateway:1.14.2 4 | container_name: gateway 5 | networks: 6 | - test 7 | ports: 8 | - "0.0.0.0:4000:4000" 9 | environment: 10 | - NODE_ENV=production 11 | - FORK=${FORK} 12 | - JIT=true 13 | depends_on: 14 | accounts: 15 | condition: service_healthy 16 | inventory: 17 | condition: service_healthy 18 | products: 19 | condition: service_healthy 20 | reviews: 21 | condition: service_healthy 22 | volumes: 23 | - type: bind 24 | source: federation/gateways/hive-gateway/supergraph.graphql 25 | target: /serve/supergraph.graphql 26 | healthcheck: 27 | test: 28 | [ 29 | "CMD", 30 | "/usr/lib/apt/apt-helper", 31 | "download-file", 32 | "http://127.0.0.1:4000/graphql?query=%7B+users+%7B+reviews+%7B+product+%7B+reviews+%7B+author+%7B+reviews+%7B+product+%7B+name+%7D+%7D+%7D+%7D+%7D+%7D+%7D+%7D", 33 | "/tmp/health", 34 | ] 35 | interval: 3s 36 | timeout: 5s 37 | retries: 10 38 | command: ["supergraph", "--jit"] 39 | deploy: 40 | resources: 41 | limits: 42 | cpus: ${CPU_LIMIT:-1} 43 | memory: ${MEM_LIMIT:-1gb} 44 | networks: 45 | test: 46 | name: test 47 | -------------------------------------------------------------------------------- /federation/gateways/hive-gateway/supergraph.graphql: -------------------------------------------------------------------------------- 1 | schema 2 | @link(url: "https://specs.apollo.dev/link/v1.0") 3 | @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) 4 | { 5 | query: Query 6 | } 7 | 8 | directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE 9 | 10 | directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION 11 | 12 | directive @join__graph(name: String!, url: String!) on ENUM_VALUE 13 | 14 | directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE 15 | 16 | directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR 17 | 18 | directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION 19 | 20 | directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA 21 | 22 | scalar join__FieldSet 23 | 24 | enum join__Graph { 25 | ACCOUNTS @join__graph(name: "accounts", url: "http://accounts:4001/graphql") 26 | INVENTORY @join__graph(name: "inventory", url: "http://inventory:4002/graphql") 27 | PRODUCTS @join__graph(name: "products", url: "http://products:4003/graphql") 28 | REVIEWS @join__graph(name: "reviews", url: "http://reviews:4004/graphql") 29 | } 30 | 31 | scalar link__Import 32 | 33 | enum link__Purpose { 34 | """ 35 | `SECURITY` features provide metadata necessary to securely resolve fields. 36 | """ 37 | SECURITY 38 | 39 | """ 40 | `EXECUTION` features provide metadata necessary for operation execution. 41 | """ 42 | EXECUTION 43 | } 44 | 45 | type Product 46 | @join__type(graph: INVENTORY, key: "upc") 47 | @join__type(graph: PRODUCTS, key: "upc") 48 | @join__type(graph: REVIEWS, key: "upc") 49 | { 50 | upc: String! 51 | weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) 52 | price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS) 53 | inStock: Boolean @join__field(graph: INVENTORY) 54 | shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") 55 | name: String @join__field(graph: PRODUCTS) 56 | reviews: [Review] @join__field(graph: REVIEWS) 57 | } 58 | 59 | type Query 60 | @join__type(graph: ACCOUNTS) 61 | @join__type(graph: INVENTORY) 62 | @join__type(graph: PRODUCTS) 63 | @join__type(graph: REVIEWS) 64 | { 65 | me: User @join__field(graph: ACCOUNTS) 66 | user(id: ID!): User @join__field(graph: ACCOUNTS) 67 | users: [User] @join__field(graph: ACCOUNTS) 68 | topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) 69 | } 70 | 71 | type Review 72 | @join__type(graph: REVIEWS, key: "id") 73 | { 74 | id: ID! 75 | body: String 76 | product: Product 77 | author: User @join__field(graph: REVIEWS, provides: "username") 78 | } 79 | 80 | type User 81 | @join__type(graph: ACCOUNTS, key: "id") 82 | @join__type(graph: REVIEWS, key: "id") 83 | { 84 | id: ID! 85 | name: String @join__field(graph: ACCOUNTS) 86 | username: String @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS, external: true) 87 | birthday: Int @join__field(graph: ACCOUNTS) 88 | reviews: [Review] @join__field(graph: REVIEWS) 89 | } -------------------------------------------------------------------------------- /federation/gateways/mercurius/.yarnrc: -------------------------------------------------------------------------------- 1 | ignore-engines true -------------------------------------------------------------------------------- /federation/gateways/mercurius/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:23-slim 2 | 3 | RUN apt-get update -y && apt-get install curl -y 4 | 5 | WORKDIR /home/gw 6 | COPY index.ts ./ 7 | COPY package.json yarn.lock ./ 8 | 9 | RUN yarn install --production 10 | ENV PORT=4000 11 | ENV NODE_ENV=production 12 | 13 | EXPOSE 4000 14 | 15 | WORKDIR /home/gw 16 | 17 | CMD ["yarn", "start"] 18 | -------------------------------------------------------------------------------- /federation/gateways/mercurius/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | gateway: 3 | image: gateway/mercurius 4 | container_name: gateway 5 | networks: 6 | - test 7 | ports: 8 | - "0.0.0.0:4000:4000" 9 | environment: 10 | PORT: 4000 11 | build: 12 | context: ${BASE_DIR:-.}/../../gateways/mercurius 13 | dockerfile: ./Dockerfile 14 | depends_on: 15 | accounts: 16 | condition: service_healthy 17 | inventory: 18 | condition: service_healthy 19 | products: 20 | condition: service_healthy 21 | reviews: 22 | condition: service_healthy 23 | healthcheck: 24 | test: ["CMD", "curl", "-f", "-X", "POST", "-H" ,"Content-type: application/json", "http://localhost:4000/graphql", "--data-raw", "{\"query\": \"query { __typename }\"}"] 25 | interval: 3s 26 | timeout: 5s 27 | retries: 10 28 | deploy: 29 | resources: 30 | limits: 31 | cpus: ${CPU_LIMIT:-1} 32 | memory: ${MEM_LIMIT:-1gb} 33 | networks: 34 | test: 35 | name: test -------------------------------------------------------------------------------- /federation/gateways/mercurius/index.ts: -------------------------------------------------------------------------------- 1 | import Fastify from "fastify"; 2 | import mercuriusWithGateway from "@mercuriusjs/gateway"; 3 | 4 | const port = process.env.PORT ? parseInt(process.env.PORT) : 4000; 5 | const gateway = Fastify(); 6 | 7 | gateway.register(mercuriusWithGateway, { 8 | graphiql: true, 9 | jit: 1, 10 | path: "/graphql", 11 | gateway: { 12 | services: [ 13 | { 14 | name: "accounts", 15 | url: "http://accounts:4001/graphql", 16 | mandatory: true, 17 | }, 18 | { 19 | name: "reviews", 20 | url: "http://reviews:4004/graphql", 21 | mandatory: true, 22 | }, 23 | { 24 | name: "products", 25 | url: "http://products:4003/graphql", 26 | mandatory: true, 27 | }, 28 | { 29 | name: "inventory", 30 | url: "http://inventory:4002/graphql", 31 | mandatory: true, 32 | }, 33 | ], 34 | }, 35 | }); 36 | 37 | gateway.listen({ port, host: "0.0.0.0" }); 38 | -------------------------------------------------------------------------------- /federation/gateways/mercurius/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mercurius", 3 | "scripts": { 4 | "start": "tsx index.ts" 5 | }, 6 | "dependencies": { 7 | "tsx": "4.19.4", 8 | "typescript": "5.8.3", 9 | "graphql": "16.11.0", 10 | "fastify": "5.3.3", 11 | "@mercuriusjs/gateway": "5.0.0" 12 | }, 13 | "resolutions": { 14 | "graphql": "16.11.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /federation/scenarios/compose-supergraph.ts: -------------------------------------------------------------------------------- 1 | import { composeServices } from '@apollo/composition' 2 | import { DocumentNode, Kind, SchemaExtensionNode, parse } from 'graphql' 3 | import { writeFileSync } from 'node:fs' 4 | import { print } from 'graphql'; 5 | 6 | function cleanupFedV1(doc: DocumentNode): DocumentNode { 7 | (doc as any).definitions = doc.definitions.filter(d => d.kind != Kind.SCHEMA_EXTENSION); 8 | 9 | return doc; 10 | } 11 | 12 | const FED_V1_DIRECTIVES = ` 13 | scalar _Any 14 | scalar _FieldSet 15 | 16 | directive @external on FIELD_DEFINITION 17 | directive @requires(fields: _FieldSet!) on FIELD_DEFINITION 18 | directive @provides(fields: _FieldSet!) on FIELD_DEFINITION 19 | directive @key(fields: _FieldSet!) on OBJECT | INTERFACE 20 | directive @extends on OBJECT 21 | `; 22 | 23 | async function main() { 24 | let accountsSdl = await fetch('http://127.0.0.1:4001/sdl').then(r => r.text()).then(parse).then(cleanupFedV1); 25 | let inventorySdl = await fetch('http://127.0.0.1:4002/sdl').then(r => r.text()).then(parse).then(cleanupFedV1); 26 | let productsSdl = await fetch('http://127.0.0.1:4003/sdl').then(r => r.text()).then(parse).then(cleanupFedV1); 27 | let reviewsSdl = await fetch('http://127.0.0.1:4004/sdl').then(r => r.text()).then(parse).then(cleanupFedV1); 28 | 29 | writeFileSync(__dirname + '/../gateways/wundergraph/.wundergraph/accounts.graphql', print(accountsSdl)); 30 | writeFileSync(__dirname + '/../gateways/wundergraph/.wundergraph/inventory.graphql', print(inventorySdl)); 31 | writeFileSync(__dirname + '/../gateways/wundergraph/.wundergraph/products.graphql', print(productsSdl)); 32 | writeFileSync(__dirname + '/../gateways/wundergraph/.wundergraph/reviews.graphql', print(reviewsSdl)); 33 | 34 | writeFileSync(__dirname + '/../gateways/mesh/config/accounts.graphql', FED_V1_DIRECTIVES + print(accountsSdl)); 35 | writeFileSync(__dirname + '/../gateways/mesh/config/inventory.graphql', FED_V1_DIRECTIVES + print(inventorySdl)); 36 | writeFileSync(__dirname + '/../gateways/mesh/config/products.graphql', FED_V1_DIRECTIVES + print(productsSdl)); 37 | writeFileSync(__dirname + '/../gateways/mesh/config/reviews.graphql', FED_V1_DIRECTIVES + print(reviewsSdl)); 38 | 39 | const { supergraphSdl, errors } = composeServices([ 40 | { 41 | name: 'reviews', 42 | typeDefs: reviewsSdl, 43 | url: 'http://reviews:4004/graphql' 44 | }, 45 | { 46 | name: 'accounts', 47 | typeDefs: accountsSdl, 48 | url: 'http://accounts:4001/graphql' 49 | }, 50 | { 51 | name: 'inventory', 52 | typeDefs: inventorySdl, 53 | url: 'http://inventory:4002/graphql' 54 | }, 55 | { 56 | name: 'products', 57 | typeDefs: productsSdl, 58 | url: 'http://products:4003/graphql' 59 | } 60 | ]); 61 | 62 | if (errors?.length && errors?.length > 0) { 63 | console.log(`Failed to compose supergraph:`, errors); 64 | throw new Error('No supergraph SDL found!') 65 | } else { 66 | console.log("Got SDL:\n", supergraphSdl); 67 | writeFileSync(__dirname + '/../gateways/apollo-router/supergraph.graphql', supergraphSdl!); 68 | writeFileSync(__dirname + '/../gateways/hive-gateway/supergraph.graphql', supergraphSdl!); 69 | writeFileSync(__dirname + '/../gateways/hive-gateway-bun/supergraph.graphql', supergraphSdl!); 70 | writeFileSync(__dirname + '/../gateways/grafbase/supergraph.graphql', supergraphSdl!); 71 | writeFileSync(__dirname + '/../gateways/apollo-server/supergraph.graphql', supergraphSdl!); 72 | writeFileSync(__dirname + '/../gateways/apollo-server-node16/supergraph.graphql', supergraphSdl!); 73 | writeFileSync(__dirname + '/../gateways/mesh-supergraph/supergraph.graphql', supergraphSdl!); 74 | } 75 | } 76 | 77 | main().catch(console.error) 78 | -------------------------------------------------------------------------------- /federation/scenarios/constant-vus-over-time/README.md: -------------------------------------------------------------------------------- 1 | ## Overview for: `federation/constant-vus-over-time` 2 | 3 | 4 | This scenario runs 4 subgraphs and a GraphQL gateway with Federation spec, and runs a heavy query. It's being executed with a constant amount of VUs over a fixed amount of time. It measure things like memory usage, CPU usage, average RPS. It also includes a summary of the entire execution, and metrics information about HTTP execution times. 5 | 6 | 7 | This scenario was running 300 VUs over 60s 8 | 9 | 10 | ### Comparison 11 | 12 | 13 | Comparison 14 | 15 | 16 | | Gateway | RPS ⬇️ | Requests | Duration | Notes | 17 | | :--------------- | :----: | :-------------------: | :----------------------: | :----------------------------------------------------------------------- | 18 | | cosmo | 173 | 10567 total, 0 failed | avg: 878ms, p95: 2464ms | ✅ | 19 | | grafbase | 165 | 10077 total, 0 failed | avg: 804ms, p95: 2348ms | ✅ | 20 | | apollo-router | 153 | 9404 total, 0 failed | avg: 960ms, p95: 2429ms | ✅ | 21 | | hive-gateway-bun | 92 | 5813 total, 0 failed | avg: 3124ms, p95: 5372ms | ✅ | 22 | | apollo-server | 87 | 5485 total, 71 failed | avg: 3311ms, p95: 2959ms | ❌ 71 failed requests, 71 non-200 responses, 71 unexpected GraphQL errors | 23 | | hive-gateway | 86 | 5435 total, 8 failed | avg: 3364ms, p95: 5008ms | ❌ 8 failed requests, 8 non-200 responses, 8 unexpected GraphQL errors | 24 | | mercurius | 76 | 4760 total, 0 failed | avg: 3811ms, p95: 4950ms | ✅ | 25 | 26 | 27 | 28 |
29 | Summary for: `cosmo` 30 | 31 | **K6 Output** 32 | 33 | 34 | 35 | 36 | ``` 37 | ✓ response code was 200 38 | ✓ no graphql errors 39 | ✓ valid response structure 40 | 41 | █ setup 42 | 43 | checks.........................: 100.00% ✓ 31641 ✗ 0 44 | data_received..................: 927 MB 15 MB/s 45 | data_sent......................: 13 MB 206 kB/s 46 | http_req_blocked...............: avg=1.57ms min=2µs med=4.18µs max=4.86s p(90)=6.12µs p(95)=12.32µs 47 | http_req_connecting............: avg=952.16µs min=0s med=0s max=1.26s p(90)=0s p(95)=0s 48 | http_req_duration..............: avg=878.11ms min=3.9ms med=666.63ms max=6.66s p(90)=1.88s p(95)=2.46s 49 | { expected_response:true }...: avg=878.11ms min=3.9ms med=666.63ms max=6.66s p(90)=1.88s p(95)=2.46s 50 | http_req_failed................: 0.00% ✓ 0 ✗ 10567 51 | http_req_receiving.............: avg=354.41ms min=33.39µs med=112.89µs max=5.63s p(90)=1.31s p(95)=1.94s 52 | http_req_sending...............: avg=22.82ms min=8.86µs med=21.31µs max=3.98s p(90)=153.77µs p(95)=14.96ms 53 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 54 | http_req_waiting...............: avg=500.87ms min=3.81ms med=451.85ms max=2.74s p(90)=947.09ms p(95)=1.07s 55 | http_reqs......................: 10567 173.232649/s 56 | iteration_duration.............: avg=1.71s min=18.92ms med=1.35s max=12.06s p(90)=3.6s p(95)=4.54s 57 | iterations.....................: 10547 172.904774/s 58 | vus............................: 6 min=6 max=300 59 | vus_max........................: 300 min=300 max=300 60 | ``` 61 | 62 | 63 | **Performance Overview** 64 | 65 | 66 | Performance Overview 67 | 68 | 69 | **Subgraphs Overview** 70 | 71 | 72 | Subgraphs Overview 73 | 74 | 75 | **HTTP Overview** 76 | 77 | 78 | HTTP Overview 79 | 80 | 81 |
82 | 83 |
84 | Summary for: `grafbase` 85 | 86 | **K6 Output** 87 | 88 | 89 | 90 | 91 | ``` 92 | ✓ response code was 200 93 | ✓ no graphql errors 94 | ✓ valid response structure 95 | 96 | █ setup 97 | 98 | checks.........................: 100.00% ✓ 30171 ✗ 0 99 | data_received..................: 886 MB 15 MB/s 100 | data_sent......................: 12 MB 196 kB/s 101 | http_req_blocked...............: avg=1.51ms min=1.71µs med=4.2µs max=1.41s p(90)=6.17µs p(95)=13.39µs 102 | http_req_connecting............: avg=1.15ms min=0s med=0s max=1.26s p(90)=0s p(95)=0s 103 | http_req_duration..............: avg=804.01ms min=3.21ms med=604.7ms max=6.76s p(90)=1.71s p(95)=2.34s 104 | { expected_response:true }...: avg=804.01ms min=3.21ms med=604.7ms max=6.76s p(90)=1.71s p(95)=2.34s 105 | http_req_failed................: 0.00% ✓ 0 ✗ 10077 106 | http_req_receiving.............: avg=317.08ms min=29.21µs med=106.21µs max=5.7s p(90)=1.15s p(95)=1.7s 107 | http_req_sending...............: avg=27.29ms min=8.58µs med=22.3µs max=4.03s p(90)=260.55µs p(95)=41.61ms 108 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 109 | http_req_waiting...............: avg=459.63ms min=3.12ms med=408.57ms max=2.65s p(90)=914.11ms p(95)=1.07s 110 | http_reqs......................: 10077 165.305669/s 111 | iteration_duration.............: avg=1.78s min=20.8ms med=1.4s max=13.56s p(90)=3.82s p(95)=4.81s 112 | iterations.....................: 10057 164.977584/s 113 | vus............................: 277 min=277 max=300 114 | vus_max........................: 300 min=300 max=300 115 | ``` 116 | 117 | 118 | **Performance Overview** 119 | 120 | 121 | Performance Overview 122 | 123 | 124 | **Subgraphs Overview** 125 | 126 | 127 | Subgraphs Overview 128 | 129 | 130 | **HTTP Overview** 131 | 132 | 133 | HTTP Overview 134 | 135 | 136 |
137 | 138 |
139 | Summary for: `apollo-router` 140 | 141 | **K6 Output** 142 | 143 | 144 | 145 | 146 | ``` 147 | ✓ response code was 200 148 | ✓ no graphql errors 149 | ✓ valid response structure 150 | 151 | █ setup 152 | 153 | checks.........................: 100.00% ✓ 28152 ✗ 0 154 | data_received..................: 825 MB 14 MB/s 155 | data_sent......................: 11 MB 183 kB/s 156 | http_req_blocked...............: avg=2.46ms min=1.51µs med=3.57µs max=2.32s p(90)=5.56µs p(95)=12.53µs 157 | http_req_connecting............: avg=1.7ms min=0s med=0s max=2.32s p(90)=0s p(95)=0s 158 | http_req_duration..............: avg=960.05ms min=6.58ms med=785.04ms max=6.25s p(90)=1.99s p(95)=2.42s 159 | { expected_response:true }...: avg=960.05ms min=6.58ms med=785.04ms max=6.25s p(90)=1.99s p(95)=2.42s 160 | http_req_failed................: 0.00% ✓ 0 ✗ 9404 161 | http_req_receiving.............: avg=327.7ms min=34.77µs med=91.95µs max=6.16s p(90)=1.3s p(95)=1.69s 162 | http_req_sending...............: avg=20.94ms min=7.67µs med=17.43µs max=2.91s p(90)=149.77µs p(95)=17.12ms 163 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 164 | http_req_waiting...............: avg=611.4ms min=6.49ms med=562.65ms max=3.47s p(90)=1.1s p(95)=1.38s 165 | http_reqs......................: 9404 153.982343/s 166 | iteration_duration.............: avg=1.92s min=31.42ms med=1.66s max=9.58s p(90)=3.84s p(95)=4.66s 167 | iterations.....................: 9384 153.654861/s 168 | vus............................: 37 min=37 max=300 169 | vus_max........................: 300 min=300 max=300 170 | ``` 171 | 172 | 173 | **Performance Overview** 174 | 175 | 176 | Performance Overview 177 | 178 | 179 | **Subgraphs Overview** 180 | 181 | 182 | Subgraphs Overview 183 | 184 | 185 | **HTTP Overview** 186 | 187 | 188 | HTTP Overview 189 | 190 | 191 |
192 | 193 |
194 | Summary for: `hive-gateway-bun` 195 | 196 | **K6 Output** 197 | 198 | 199 | 200 | 201 | ``` 202 | ✓ response code was 200 203 | ✓ no graphql errors 204 | ✓ valid response structure 205 | 206 | █ setup 207 | 208 | checks.........................: 100.00% ✓ 17379 ✗ 0 209 | data_received..................: 510 MB 8.1 MB/s 210 | data_sent......................: 6.9 MB 110 kB/s 211 | http_req_blocked...............: avg=695.92µs min=1.72µs med=3.94µs max=40.99ms p(90)=6.01µs p(95)=1.06ms 212 | http_req_connecting............: avg=665.01µs min=0s med=0s max=40.96ms p(90)=0s p(95)=640.82µs 213 | http_req_duration..............: avg=3.12s min=16.25ms med=2.85s max=6.76s p(90)=4.66s p(95)=5.37s 214 | { expected_response:true }...: avg=3.12s min=16.25ms med=2.85s max=6.76s p(90)=4.66s p(95)=5.37s 215 | http_req_failed................: 0.00% ✓ 0 ✗ 5813 216 | http_req_receiving.............: avg=42.6ms min=38.92µs med=165.62µs max=1.92s p(90)=3.76ms p(95)=139.95ms 217 | http_req_sending...............: avg=533.25µs min=8.99µs med=22.38µs max=150.34ms p(90)=75.53µs p(95)=667.22µs 218 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 219 | http_req_waiting...............: avg=3.08s min=16.1ms med=2.83s max=6.76s p(90)=4.62s p(95)=5.35s 220 | http_reqs......................: 5813 92.221801/s 221 | iteration_duration.............: avg=3.18s min=194.15ms med=2.89s max=6.77s p(90)=4.74s p(95)=5.42s 222 | iterations.....................: 5793 91.904506/s 223 | vus............................: 14 min=14 max=300 224 | vus_max........................: 300 min=300 max=300 225 | ``` 226 | 227 | 228 | **Performance Overview** 229 | 230 | 231 | Performance Overview 232 | 233 | 234 | **Subgraphs Overview** 235 | 236 | 237 | Subgraphs Overview 238 | 239 | 240 | **HTTP Overview** 241 | 242 | 243 | HTTP Overview 244 | 245 | 246 |
247 | 248 |
249 | Summary for: `apollo-server` 250 | 251 | **K6 Output** 252 | 253 | 254 | 255 | 256 | ``` 257 | ✗ response code was 200 258 | ↳ 98% — ✓ 5394 / ✗ 71 259 | ✗ no graphql errors 260 | ↳ 98% — ✓ 5394 / ✗ 71 261 | ✓ valid response structure 262 | 263 | █ setup 264 | 265 | checks.........................: 99.13% ✓ 16182 ✗ 142 266 | data_received..................: 476 MB 7.6 MB/s 267 | data_sent......................: 6.5 MB 104 kB/s 268 | http_req_blocked...............: avg=438.91µs min=1.58µs med=3.5µs max=41.23ms p(90)=5.54µs p(95)=1.5ms 269 | http_req_connecting............: avg=397.4µs min=0s med=0s max=30.58ms p(90)=0s p(95)=699.8µs 270 | http_req_duration..............: avg=3.31s min=10.47ms med=1.96s max=1m0s p(90)=2.5s p(95)=2.95s 271 | { expected_response:true }...: avg=2.56s min=10.47ms med=1.95s max=59.68s p(90)=2.49s p(95)=2.77s 272 | http_req_failed................: 1.29% ✓ 71 ✗ 5414 273 | http_req_receiving.............: avg=299.97µs min=0s med=106.92µs max=100.86ms p(90)=231.98µs p(95)=401.16µs 274 | http_req_sending...............: avg=141.06µs min=9.02µs med=18.74µs max=37.32ms p(90)=40.41µs p(95)=192.04µs 275 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 276 | http_req_waiting...............: avg=3.31s min=10.33ms med=1.96s max=1m0s p(90)=2.5s p(95)=2.95s 277 | http_reqs......................: 5485 87.820345/s 278 | iteration_duration.............: avg=3.34s min=183.42ms med=1.98s max=1m0s p(90)=2.52s p(95)=2.97s 279 | iterations.....................: 5465 87.500125/s 280 | vus............................: 70 min=70 max=300 281 | vus_max........................: 300 min=300 max=300 282 | ``` 283 | 284 | 285 | **Performance Overview** 286 | 287 | 288 | Performance Overview 289 | 290 | 291 | **Subgraphs Overview** 292 | 293 | 294 | Subgraphs Overview 295 | 296 | 297 | **HTTP Overview** 298 | 299 | 300 | HTTP Overview 301 | 302 | 303 |
304 | 305 |
306 | Summary for: `hive-gateway` 307 | 308 | **K6 Output** 309 | 310 | 311 | 312 | 313 | ``` 314 | ✗ response code was 200 315 | ↳ 99% — ✓ 5407 / ✗ 8 316 | ✗ no graphql errors 317 | ↳ 99% — ✓ 5407 / ✗ 8 318 | ✓ valid response structure 319 | 320 | █ setup 321 | 322 | checks.........................: 99.90% ✓ 16221 ✗ 16 323 | data_received..................: 477 MB 7.6 MB/s 324 | data_sent......................: 6.5 MB 102 kB/s 325 | http_req_blocked...............: avg=494.62µs min=1.87µs med=4.43µs max=29.84ms p(90)=6.67µs p(95)=597.24µs 326 | http_req_connecting............: avg=461.62µs min=0s med=0s max=23.64ms p(90)=0s p(95)=340.53µs 327 | http_req_duration..............: avg=3.36s min=14.47ms med=2.37s max=1m0s p(90)=3.56s p(95)=5s 328 | { expected_response:true }...: avg=3.28s min=14.47ms med=2.37s max=59.73s p(90)=3.55s p(95)=4.93s 329 | http_req_failed................: 0.14% ✓ 8 ✗ 5427 330 | http_req_receiving.............: avg=685.12µs min=0s med=109.09µs max=177.16ms p(90)=453.91µs p(95)=1.16ms 331 | http_req_sending...............: avg=284.98µs min=9.94µs med=25.2µs max=112.26ms p(90)=80.26µs p(95)=489.35µs 332 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 333 | http_req_waiting...............: avg=3.36s min=14.35ms med=2.37s max=1m0s p(90)=3.56s p(95)=5s 334 | http_reqs......................: 5435 86.106327/s 335 | iteration_duration.............: avg=3.4s min=216.78ms med=2.4s max=1m0s p(90)=3.61s p(95)=5.05s 336 | iterations.....................: 5415 85.789468/s 337 | vus............................: 22 min=22 max=300 338 | vus_max........................: 300 min=300 max=300 339 | ``` 340 | 341 | 342 | **Performance Overview** 343 | 344 | 345 | Performance Overview 346 | 347 | 348 | **Subgraphs Overview** 349 | 350 | 351 | Subgraphs Overview 352 | 353 | 354 | **HTTP Overview** 355 | 356 | 357 | HTTP Overview 358 | 359 | 360 |
361 | 362 |
363 | Summary for: `mercurius` 364 | 365 | **K6 Output** 366 | 367 | 368 | 369 | 370 | ``` 371 | ✓ response code was 200 372 | ✓ no graphql errors 373 | ✓ valid response structure 374 | 375 | █ setup 376 | 377 | checks.........................: 100.00% ✓ 14220 ✗ 0 378 | data_received..................: 418 MB 6.7 MB/s 379 | data_sent......................: 5.7 MB 91 kB/s 380 | http_req_blocked...............: avg=523.43µs min=1.82µs med=3.87µs max=20.51ms p(90)=5.97µs p(95)=1.41ms 381 | http_req_connecting............: avg=508.54µs min=0s med=0s max=19.89ms p(90)=0s p(95)=783.01µs 382 | http_req_duration..............: avg=3.81s min=10.64ms med=3.78s max=8.5s p(90)=4.42s p(95)=4.95s 383 | { expected_response:true }...: avg=3.81s min=10.64ms med=3.78s max=8.5s p(90)=4.42s p(95)=4.95s 384 | http_req_failed................: 0.00% ✓ 0 ✗ 4760 385 | http_req_receiving.............: avg=8.26ms min=39.84µs med=106.21µs max=723.47ms p(90)=349.04µs p(95)=1.18ms 386 | http_req_sending...............: avg=177.04µs min=9.74µs med=22.29µs max=25.37ms p(90)=46.04µs p(95)=378.64µs 387 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 388 | http_req_waiting...............: avg=3.8s min=10.55ms med=3.78s max=8.5s p(90)=4.42s p(95)=4.95s 389 | http_reqs......................: 4760 76.825564/s 390 | iteration_duration.............: avg=3.85s min=425.79ms med=3.8s max=8.51s p(90)=4.44s p(95)=4.97s 391 | iterations.....................: 4740 76.502768/s 392 | vus............................: 219 min=219 max=300 393 | vus_max........................: 300 min=300 max=300 394 | ``` 395 | 396 | 397 | **Performance Overview** 398 | 399 | 400 | Performance Overview 401 | 402 | 403 | **Subgraphs Overview** 404 | 405 | 406 | Subgraphs Overview 407 | 408 | 409 | **HTTP Overview** 410 | 411 | 412 | HTTP Overview 413 | 414 | 415 |
-------------------------------------------------------------------------------- /federation/scenarios/constant-vus-over-time/benchmark.k6.js: -------------------------------------------------------------------------------- 1 | import { 2 | makeGraphQLRequest, 3 | handleBenchmarkSummary, 4 | sendGraphQLRequest, 5 | } from "../k6.shared.js"; 6 | 7 | const vus = __ENV.BENCH_VUS ? parseInt(__ENV.BENCH_VUS) : 100; 8 | const time = __ENV.BENCH_OVER_TIME || "30s"; 9 | 10 | export const options = { 11 | vus: vus, 12 | duration: time, 13 | }; 14 | 15 | export function setup() { 16 | for (let i = 0; i < 20; i++) { 17 | sendGraphQLRequest(); 18 | } 19 | } 20 | 21 | export default function() { 22 | makeGraphQLRequest() 23 | } 24 | 25 | export function handleSummary(data) { 26 | return handleBenchmarkSummary(data, { vus, time }); 27 | } -------------------------------------------------------------------------------- /federation/scenarios/constant-vus-over-time/generate-report.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, readdirSync, writeFileSync } from "fs"; 2 | import { join } from "path"; 3 | import pkgJson from "./package.json"; 4 | import tablemark from "tablemark"; 5 | import * as vl from "vega-lite"; 6 | import * as v from "vega"; 7 | 8 | const IGNORED_DIRS = ["node_modules"]; 9 | const NEWLINE = "\n"; 10 | 11 | const { 12 | CF_IMAGES_LINK, 13 | CF_IMAGES_TOKEN, 14 | GITHUB_RUN_ID = "local", 15 | } = process.env; 16 | 17 | async function uploadImageToCloudflare(filename: string, filePath: string) { 18 | console.log("Uploading image to cloudflare"); 19 | const buffer = readFileSync(filePath); 20 | const blob = new Blob([buffer], { type: "image/png" }); 21 | const form = new FormData(); 22 | 23 | form.append("file", blob, filename); 24 | 25 | const res = await fetch(CF_IMAGES_LINK!, { 26 | method: "POST", 27 | body: form, 28 | headers: { 29 | Authorization: `Bearer ${CF_IMAGES_TOKEN}`, 30 | }, 31 | }); 32 | 33 | console.log(`Got a response from cloudflare (status=${res.status})`); 34 | 35 | if (!res.ok) { 36 | throw new Error(`Failed to upload image to Cloudflare: ${res.statusText}`); 37 | } 38 | 39 | const data = await res.json(); 40 | return data.result.variants[0]; 41 | } 42 | 43 | function notEmpty(value: TValue | null | undefined): value is TValue { 44 | return value !== null && value !== undefined; 45 | } 46 | 47 | function formatSummary(title: string, content: string) { 48 | return `
49 | ${title} 50 | 51 | ${content} 52 |
`; 53 | } 54 | 55 | async function generateReport(artifactsRootPath: string) { 56 | console.info(`Generating report based on artifacts in ${artifactsRootPath}`); 57 | const foundDirectories = readdirSync(artifactsRootPath, { 58 | withFileTypes: true, 59 | }) 60 | .filter((r) => r.isDirectory() && !IGNORED_DIRS.includes(r.name)) 61 | .filter( 62 | (r) => 63 | r.name.startsWith(process.env.SCENARIO_ARTIFACTS_PREFIX!) 64 | ) 65 | .map((r) => r.name); 66 | 67 | console.info( 68 | `Found the following directories to look reports in: ${foundDirectories.join( 69 | ", " 70 | )}` 71 | ); 72 | 73 | if (foundDirectories.length === 0) { 74 | throw new Error("No directories found to generate report from!"); 75 | } 76 | 77 | const reportsData = await Promise.all( 78 | foundDirectories.map(async (dirName) => { 79 | const fullPath = join(artifactsRootPath, dirName); 80 | console.log(`Processing directory: ${fullPath}`); 81 | const jsonSummaryFilePath = join(fullPath, "./k6_summary.json"); 82 | 83 | if (!existsSync(jsonSummaryFilePath)) { 84 | console.warn( 85 | `Could not find k6_summary.json in ${fullPath}! Skipping...` 86 | ); 87 | 88 | return null; 89 | } 90 | 91 | const txtSummaryFilePath = join(fullPath, "./k6_summary.txt"); 92 | 93 | let overviewImageUrl: string | null = ""; 94 | let httpImageUrl: string | null = ""; 95 | let containersImageUrl: string | null = ""; 96 | 97 | if (!CF_IMAGES_LINK || !CF_IMAGES_TOKEN) { 98 | console.warn( 99 | `Could not find CF_IMAGES_LINK or CF_IMAGES_TOKEN in env! Skipping...` 100 | ); 101 | } else { 102 | const overviewImageFilePath = join(fullPath, "./overview.png"); 103 | const httpImageFilePath = join(fullPath, "./http.png"); 104 | const containersFilePath = join(fullPath, "./containers.png"); 105 | 106 | [overviewImageUrl, httpImageUrl, containersImageUrl] = 107 | await Promise.all([ 108 | uploadImageToCloudflare( 109 | `${GITHUB_RUN_ID}-overview.png`, 110 | overviewImageFilePath 111 | ), 112 | uploadImageToCloudflare( 113 | `${GITHUB_RUN_ID}-http.png`, 114 | httpImageFilePath 115 | ), 116 | uploadImageToCloudflare( 117 | `${GITHUB_RUN_ID}-http.png`, 118 | containersFilePath 119 | ), 120 | ]); 121 | } 122 | 123 | const jsonSummary = JSON.parse(readFileSync(jsonSummaryFilePath, "utf8")); 124 | 125 | return { 126 | name: dirName.replace(process.env.SCENARIO_ARTIFACTS_PREFIX!, ""), 127 | path: fullPath, 128 | jsonSummary, 129 | txtSummary: readFileSync(txtSummaryFilePath, "utf8"), 130 | rps: Math.floor(jsonSummary.metrics.http_reqs.values.rate), 131 | overviewImageUrl, 132 | httpImageUrl, 133 | containersImageUrl, 134 | vus: jsonSummary.vus, 135 | time: jsonSummary.time, 136 | }; 137 | }) 138 | ); 139 | const validReportsData = reportsData 140 | .filter(notEmpty) 141 | .sort((a, b) => b.rps - a.rps); 142 | 143 | console.log(`Found ${validReportsData.length} valid reports`); 144 | 145 | const vega: vl.TopLevelSpec = { 146 | width: 600, 147 | height: 400, 148 | background: null as any, 149 | $schema: "https://vega.github.io/schema/vega-lite/v5.json", 150 | description: "", 151 | data: { 152 | values: validReportsData.map((v) => { 153 | return { 154 | "gateway-setup": v.name, 155 | rps: v.rps, 156 | }; 157 | }), 158 | }, 159 | mark: "bar", 160 | encoding: { 161 | x: { 162 | field: "gateway-setup", 163 | type: "nominal", 164 | sort: "-y", 165 | }, 166 | y: { 167 | field: "rps", 168 | type: "quantitative", 169 | }, 170 | }, 171 | }; 172 | 173 | const vegaSpec = vl.compile(vega).spec; 174 | const view = new v.View(v.parse(vegaSpec), { renderer: "none" }); 175 | const svg = await view.toSVG(); 176 | writeFileSync("report.svg", svg); 177 | 178 | const reportChartUrl = await uploadImageToCloudflare( 179 | `${GITHUB_RUN_ID}-report.svg`, 180 | "report.svg" 181 | ); 182 | 183 | const markdownLines: string[] = [ 184 | `## Overview for: \`${process.env.SCENARIO_TITLE}\``, 185 | NEWLINE, 186 | pkgJson.description, 187 | NEWLINE, 188 | `This scenario was running ${validReportsData[0].vus} VUs over ${validReportsData[0].time}`, 189 | NEWLINE, 190 | "### Comparison", 191 | NEWLINE, 192 | reportChartUrl 193 | ? `Comparison` 194 | : "**no-chart-available**", 195 | NEWLINE, 196 | tablemark( 197 | validReportsData.map((v) => { 198 | const notes: string[] = []; 199 | 200 | if (v.jsonSummary.metrics.http_req_failed.values.passes > 0) { 201 | notes.push( 202 | `${v.jsonSummary.metrics.http_req_failed.values.passes} failed requests` 203 | ); 204 | } 205 | 206 | const checks: Array<{ 207 | fails: number; 208 | name: string; 209 | }> = v.jsonSummary.root_group.checks; 210 | const http200Check = checks.find( 211 | (c) => c.name === "response code was 200" 212 | ); 213 | const graphqlErrors = checks.find( 214 | (c) => c.name === "no graphql errors" 215 | ); 216 | const responseStructure = checks.find( 217 | (c) => c.name === "valid response structure" 218 | ); 219 | 220 | 221 | if (http200Check.fails > 0) { 222 | notes.push(`${http200Check.fails} non-200 responses`); 223 | } 224 | 225 | if (graphqlErrors.fails > 0) { 226 | notes.push(`${graphqlErrors.fails} unexpected GraphQL errors`); 227 | } 228 | 229 | if (responseStructure.fails > 0) { 230 | notes.push( 231 | `non-compatible response structure (${responseStructure.fails})` 232 | ); 233 | } 234 | 235 | return { 236 | gw: v.name, 237 | rps: Math.round(v.rps), 238 | requests: `${v.jsonSummary.metrics.http_reqs.values.count} total, ${v.jsonSummary.metrics.http_req_failed.values.passes} failed`, 239 | duration: `avg: ${Math.round( 240 | v.jsonSummary.metrics.http_req_duration.values.avg 241 | )}ms, p95: ${Math.round( 242 | v.jsonSummary.metrics.http_req_duration.values["p(95)"] 243 | )}ms`, 244 | notes: notes.length === 0 ? "✅" : "❌ " + notes.join(", "), 245 | }; 246 | }), 247 | { 248 | columns: [ 249 | { name: "Gateway" }, 250 | { name: "RPS ⬇️", align: "center" }, 251 | { name: "Requests", align: "center" }, 252 | { name: "Duration", align: "center" }, 253 | { name: "Notes", align: "left" }, 254 | ], 255 | } 256 | ), 257 | NEWLINE, 258 | validReportsData 259 | .map((info) => 260 | formatSummary( 261 | `Summary for: \`${info.name}\``, 262 | [ 263 | "**K6 Output**", 264 | NEWLINE, 265 | NEWLINE, 266 | "```", 267 | info.txtSummary, 268 | "```", 269 | NEWLINE, 270 | "**Performance Overview**", 271 | NEWLINE, 272 | info.overviewImageUrl 273 | ? `Performance Overview` 274 | : "**no-image-available**", 275 | NEWLINE, 276 | "**Subgraphs Overview**", 277 | NEWLINE, 278 | info.containersImageUrl 279 | ? `Subgraphs Overview` 280 | : "**no-image-available**", 281 | NEWLINE, 282 | "**HTTP Overview**", 283 | NEWLINE, 284 | info.httpImageUrl 285 | ? `HTTP Overview` 286 | : "**no-image-available**", 287 | NEWLINE, 288 | ].join("\n") 289 | ) 290 | ) 291 | .join("\n\n"), 292 | ]; 293 | 294 | const markdown = markdownLines.join("\n"); 295 | writeFileSync("result.md", markdown); 296 | writeFileSync("report.vega.json", JSON.stringify(vega, null, 2)); 297 | } 298 | 299 | const artifactsRootPath = process.argv[2] || __dirname; 300 | 301 | generateReport(artifactsRootPath).catch(console.error); 302 | -------------------------------------------------------------------------------- /federation/scenarios/constant-vus-over-time/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fed-constant-vus-over-time", 3 | "description": "This scenario runs 4 subgraphs and a GraphQL gateway with Federation spec, and runs a heavy query. It's being executed with a constant amount of VUs over a fixed amount of time. It measure things like memory usage, CPU usage, average RPS. It also includes a summary of the entire execution, and metrics information about HTTP execution times.", 4 | "scripts": { 5 | "generate-report": "tsx generate-report.ts", 6 | "postinstall": "patch-package" 7 | }, 8 | "dependencies": { 9 | "vega": "6.1.2", 10 | "vega-lite": "6.1.0", 11 | "tablemark": "3.1.0", 12 | "tsx": "4.19.4", 13 | "typescript": "5.8.3", 14 | "graphql": "16.11.0", 15 | "patch-package": "8.0.0" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "22.15.24" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /federation/scenarios/constant-vus-over-time/patches/vega-canvas+2.0.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/vega-canvas/build/vega-canvas.node.js b/node_modules/vega-canvas/build/vega-canvas.node.js 2 | index 0dad5f9..65ecadd 100644 3 | --- a/node_modules/vega-canvas/build/vega-canvas.node.js 4 | +++ b/node_modules/vega-canvas/build/vega-canvas.node.js 5 | @@ -13,7 +13,7 @@ const domImage = () => typeof Image !== 'undefined' ? Image : null; 6 | 7 | let NodeCanvas; 8 | try { 9 | - NodeCanvas = await import('canvas'); 10 | + NodeCanvas = require('canvas'); 11 | if (!(NodeCanvas && NodeCanvas.createCanvas)) { 12 | NodeCanvas = null; 13 | } 14 | -------------------------------------------------------------------------------- /federation/scenarios/constant-vus-over-time/run.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | set -e 3 | 4 | if [ ! -n "$1" ]; then 5 | echo "gateway dir is missing, please run as: ./run.sh " 6 | exit 1 7 | fi 8 | 9 | export BASE_DIR=$( realpath ./ ) 10 | 11 | export LOCAL_ENV_OVERRIDE="" 12 | 13 | if [[ -z "${CI}" ]]; then 14 | export LOCAL_ENV_OVERRIDE="-f ../../subgraphs/docker-compose.subgraphs.local.yaml" 15 | fi 16 | 17 | export OUT_DIR="../../gateways/$1" 18 | export COMPOSE_FLAGS="-f ../../../docker-compose.metrics.yaml -f ../../subgraphs/docker-compose.subgraphs.yaml $LOCAL_ENV_OVERRIDE -f $OUT_DIR/docker-compose.yaml" 19 | 20 | on_error(){ 21 | cd $BASE_DIR 22 | docker inspect gateway 23 | docker inspect accounts 24 | docker compose $COMPOSE_FLAGS ps -a 25 | docker compose $COMPOSE_FLAGS logs 26 | } 27 | 28 | trap 'on_error' ERR 29 | 30 | docker compose $COMPOSE_FLAGS up -d --wait --force-recreate --build 31 | 32 | if [[ -z "${CI}" ]]; then 33 | trap "docker compose $COMPOSE_FLAGS down && exit 0" INT 34 | fi 35 | 36 | export K6_PROMETHEUS_RW_SERVER_URL=http://localhost:9090/api/v1/write 37 | export K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM=true 38 | export START_TIME="$(date +%s)" 39 | 40 | k6 --out=experimental-prometheus-rw --out json=$OUT_DIR/k6_metrics.json run -e SUMMARY_PATH="$OUT_DIR" benchmark.k6.js 41 | 42 | sleep 2 43 | 44 | export END_TIME="$(date +%s)" 45 | 46 | docker logs gateway > $OUT_DIR/gateway_log.txt 47 | 48 | rm -rf $OUT_DIR/overview.png || echo "" 49 | npx --quiet capture-website-cli "http://localhost:3000/d/01npcT44k/k6?orgId=1&from=${START_TIME}000&to=${END_TIME}000&kiosk" --output $OUT_DIR/overview.png --width 1200 --height 850 50 | 51 | rm -rf $OUT_DIR/http.png || echo "" 52 | npx --quiet capture-website-cli "http://localhost:3000/d-solo/01npcT44k/k6?orgId=1&from=${START_TIME}000&to=${END_TIME}000&panelId=41" --output $OUT_DIR/http.png --width 1200 --height 850 53 | 54 | rm -rf $OUT_DIR/containers.png || echo "" 55 | npx --quiet capture-website-cli "http://localhost:3000/d/pMEd7m0Mz/cadvisor-exporter?orgId=1&var-host=All&var-container=accounts&var-container=inventory&var-container=products&var-container=reviews&from=${START_TIME}000&to=${END_TIME}000&kiosk" --output $OUT_DIR/containers.png --width 1200 --height 850 56 | 57 | if [[ -z "${CI}" ]]; then 58 | echo "Done, you can find some stats in Grafana: http://localhost:3000/d/01npcT44k/k6?orgId=1&from=${START_TIME}000&to=${END_TIME}000" 59 | echo "You can close this and terminate all running services by using Ctrl+C" 60 | while true; do sleep 10; done 61 | fi 62 | -------------------------------------------------------------------------------- /federation/scenarios/constant-vus-subgraphs-delay-resources/README.md: -------------------------------------------------------------------------------- 1 | ## Overview for: `federation/constant-vus-subgraphs-delay-resources` 2 | 3 | 4 | This scenario runs 4 subgraphs and a GraphQL gateway with Federation spec, and runs a heavy query. It's being executed with a constant amount of VUs over a fixed amount of time. It measure things like memory usage, CPU usage, average RPS. It also includes a summary of the entire execution, and metrics information about HTTP execution times. 5 | 6 | 7 | This scenario was running 500 VUs over 60s 8 | 9 | 10 | ### Comparison 11 | 12 | 13 | Comparison 14 | 15 | 16 | | Gateway | RPS ⬇️ | Requests | Duration | Notes | 17 | | :--------------- | :----: | :--------------------: | :-----------------------: | :-------------------------------------------------------------------------------------------------------------- | 18 | | cosmo | 174 | 10739 total, 0 failed | avg: 1292ms, p95: 2891ms | ✅ | 19 | | grafbase | 167 | 10280 total, 0 failed | avg: 1456ms, p95: 3507ms | ✅ | 20 | | apollo-router | 158 | 9722 total, 14 failed | avg: 1363ms, p95: 3432ms | ❌ 14 failed requests, 14 non-200 responses, 16 unexpected GraphQL errors, non-compatible response structure (2) | 21 | | hive-gateway-bun | 91 | 5935 total, 0 failed | avg: 5118ms, p95: 8376ms | ✅ | 22 | | apollo-server | 89 | 5625 total, 194 failed | avg: 5395ms, p95: 8340ms | ❌ 194 failed requests, 194 non-200 responses, 194 unexpected GraphQL errors | 23 | | hive-gateway | 89 | 5735 total, 123 failed | avg: 5304ms, p95: 18643ms | ❌ 123 failed requests, 123 non-200 responses, 123 unexpected GraphQL errors | 24 | | mercurius | 76 | 4800 total, 0 failed | avg: 6356ms, p95: 8422ms | ✅ | 25 | 26 | 27 | 28 |
29 | Summary for: `cosmo` 30 | 31 | **K6 Output** 32 | 33 | 34 | 35 | 36 | ``` 37 | ✓ response code was 200 38 | ✓ no graphql errors 39 | ✓ valid response structure 40 | 41 | █ setup 42 | 43 | checks.........................: 100.00% ✓ 32157 ✗ 0 44 | data_received..................: 943 MB 15 MB/s 45 | data_sent......................: 13 MB 207 kB/s 46 | http_req_blocked...............: avg=2.63ms min=1.48µs med=3.58µs max=2.22s p(90)=5.9µs p(95)=172.26µs 47 | http_req_connecting............: avg=2.26ms min=0s med=0s max=1.55s p(90)=0s p(95)=0s 48 | http_req_duration..............: avg=1.29s min=3.33ms med=1.1s max=7.54s p(90)=2.25s p(95)=2.89s 49 | { expected_response:true }...: avg=1.29s min=3.33ms med=1.1s max=7.54s p(90)=2.25s p(95)=2.89s 50 | http_req_failed................: 0.00% ✓ 0 ✗ 10739 51 | http_req_receiving.............: avg=271.08ms min=32.87µs med=83.56µs max=6.94s p(90)=1.04s p(95)=1.82s 52 | http_req_sending...............: avg=27.56ms min=8.26µs med=16.61µs max=4.86s p(90)=338.82µs p(95)=11.49ms 53 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 54 | http_req_waiting...............: avg=993.61ms min=3.15ms med=953.27ms max=4.12s p(90)=1.64s p(95)=1.83s 55 | http_reqs......................: 10739 174.150832/s 56 | iteration_duration.............: avg=2.81s min=41.43ms med=2.44s max=13.15s p(90)=5.24s p(95)=6.36s 57 | iterations.....................: 10719 173.826499/s 58 | vus............................: 326 min=326 max=500 59 | vus_max........................: 500 min=500 max=500 60 | ``` 61 | 62 | 63 | **Performance Overview** 64 | 65 | 66 | Performance Overview 67 | 68 | 69 | **Subgraphs Overview** 70 | 71 | 72 | Subgraphs Overview 73 | 74 | 75 | **HTTP Overview** 76 | 77 | 78 | HTTP Overview 79 | 80 | 81 |
82 | 83 |
84 | Summary for: `grafbase` 85 | 86 | **K6 Output** 87 | 88 | 89 | 90 | 91 | ``` 92 | ✓ response code was 200 93 | ✓ no graphql errors 94 | ✓ valid response structure 95 | 96 | █ setup 97 | 98 | checks.........................: 100.00% ✓ 30780 ✗ 0 99 | data_received..................: 903 MB 15 MB/s 100 | data_sent......................: 12 MB 198 kB/s 101 | http_req_blocked...............: avg=2.88ms min=1.49µs med=3.66µs max=3.94s p(90)=5.96µs p(95)=248.12µs 102 | http_req_connecting............: avg=1.74ms min=0s med=0s max=2.43s p(90)=0s p(95)=0s 103 | http_req_duration..............: avg=1.45s min=3.11ms med=1.25s max=7.95s p(90)=2.93s p(95)=3.5s 104 | { expected_response:true }...: avg=1.45s min=3.11ms med=1.25s max=7.95s p(90)=2.93s p(95)=3.5s 105 | http_req_failed................: 0.00% ✓ 0 ✗ 10280 106 | http_req_receiving.............: avg=338.48ms min=33.01µs med=88.23µs max=5.72s p(90)=1.25s p(95)=1.97s 107 | http_req_sending...............: avg=29.06ms min=7.54µs med=17.29µs max=5.51s p(90)=182.97µs p(95)=6.52ms 108 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 109 | http_req_waiting...............: avg=1.08s min=3.04ms med=1.01s max=5.14s p(90)=1.97s p(95)=2.44s 110 | http_reqs......................: 10280 167.075394/s 111 | iteration_duration.............: avg=2.91s min=26.2ms med=2.5s max=18.6s p(90)=5.63s p(95)=6.85s 112 | iterations.....................: 10260 166.750345/s 113 | vus............................: 239 min=239 max=500 114 | vus_max........................: 500 min=500 max=500 115 | ``` 116 | 117 | 118 | **Performance Overview** 119 | 120 | 121 | Performance Overview 122 | 123 | 124 | **Subgraphs Overview** 125 | 126 | 127 | Subgraphs Overview 128 | 129 | 130 | **HTTP Overview** 131 | 132 | 133 | HTTP Overview 134 | 135 | 136 |
137 | 138 |
139 | Summary for: `apollo-router` 140 | 141 | **K6 Output** 142 | 143 | 144 | 145 | 146 | ``` 147 | ✗ response code was 200 148 | ↳ 99% — ✓ 9688 / ✗ 14 149 | ✗ no graphql errors 150 | ↳ 99% — ✓ 9686 / ✗ 16 151 | ✗ valid response structure 152 | ↳ 99% — ✓ 9687 / ✗ 2 153 | 154 | █ setup 155 | 156 | checks.........................: 99.89% ✓ 29061 ✗ 32 157 | data_received..................: 852 MB 14 MB/s 158 | data_sent......................: 12 MB 188 kB/s 159 | http_req_blocked...............: avg=8.6ms min=1.62µs med=3.76µs max=3.7s p(90)=6.29µs p(95)=12.9ms 160 | http_req_connecting............: avg=7.5ms min=0s med=0s max=3.7s p(90)=0s p(95)=11.12ms 161 | http_req_duration..............: avg=1.36s min=7.05ms med=1.13s max=7.43s p(90)=2.68s p(95)=3.43s 162 | { expected_response:true }...: avg=1.36s min=7.05ms med=1.13s max=7.43s p(90)=2.68s p(95)=3.43s 163 | http_req_failed................: 0.14% ✓ 14 ✗ 9708 164 | http_req_receiving.............: avg=344.27ms min=0s med=88.36µs max=6.77s p(90)=1.45s p(95)=2.06s 165 | http_req_sending...............: avg=37.64ms min=7.65µs med=17.87µs max=5.16s p(90)=322.23µs p(95)=20.54ms 166 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 167 | http_req_waiting...............: avg=981.39ms min=6.97ms med=886.14ms max=4.3s p(90)=1.71s p(95)=2.13s 168 | http_reqs......................: 9722 158.16236/s 169 | iteration_duration.............: avg=3.06s min=44.68ms med=2.74s max=14.93s p(90)=5.8s p(95)=6.79s 170 | iterations.....................: 9702 157.83699/s 171 | vus............................: 168 min=168 max=500 172 | vus_max........................: 500 min=500 max=500 173 | ``` 174 | 175 | 176 | **Performance Overview** 177 | 178 | 179 | Performance Overview 180 | 181 | 182 | **Subgraphs Overview** 183 | 184 | 185 | Subgraphs Overview 186 | 187 | 188 | **HTTP Overview** 189 | 190 | 191 | HTTP Overview 192 | 193 | 194 |
195 | 196 |
197 | Summary for: `hive-gateway-bun` 198 | 199 | **K6 Output** 200 | 201 | 202 | 203 | 204 | ``` 205 | ✓ response code was 200 206 | ✓ no graphql errors 207 | ✓ valid response structure 208 | 209 | █ setup 210 | 211 | checks.........................: 100.00% ✓ 17745 ✗ 0 212 | data_received..................: 521 MB 8.0 MB/s 213 | data_sent......................: 7.0 MB 109 kB/s 214 | http_req_blocked...............: avg=1.17ms min=1.78µs med=4.22µs max=307.07ms p(90)=12.11µs p(95)=10.12ms 215 | http_req_connecting............: avg=1.08ms min=0s med=0s max=49.93ms p(90)=0s p(95)=9.83ms 216 | http_req_duration..............: avg=5.11s min=14.96ms med=4.71s max=11.15s p(90)=7.72s p(95)=8.37s 217 | { expected_response:true }...: avg=5.11s min=14.96ms med=4.71s max=11.15s p(90)=7.72s p(95)=8.37s 218 | http_req_failed................: 0.00% ✓ 0 ✗ 5935 219 | http_req_receiving.............: avg=83.22ms min=41.49µs med=108.35µs max=3.06s p(90)=15.98ms p(95)=575.44ms 220 | http_req_sending...............: avg=2.01ms min=9.31µs med=22.65µs max=690.55ms p(90)=347.33µs p(95)=3.07ms 221 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 222 | http_req_waiting...............: avg=5.03s min=14.71ms med=4.67s max=11.09s p(90)=7.7s p(95)=8.33s 223 | http_reqs......................: 5935 91.46241/s 224 | iteration_duration.............: avg=5.29s min=299.06ms med=4.83s max=11.21s p(90)=7.99s p(95)=8.62s 225 | iterations.....................: 5915 91.154197/s 226 | vus............................: 181 min=181 max=500 227 | vus_max........................: 500 min=500 max=500 228 | ``` 229 | 230 | 231 | **Performance Overview** 232 | 233 | 234 | Performance Overview 235 | 236 | 237 | **Subgraphs Overview** 238 | 239 | 240 | Subgraphs Overview 241 | 242 | 243 | **HTTP Overview** 244 | 245 | 246 | HTTP Overview 247 | 248 | 249 |
250 | 251 |
252 | Summary for: `apollo-server` 253 | 254 | **K6 Output** 255 | 256 | 257 | 258 | 259 | ``` 260 | ✗ response code was 200 261 | ↳ 96% — ✓ 5411 / ✗ 194 262 | ✗ no graphql errors 263 | ↳ 96% — ✓ 5411 / ✗ 194 264 | ✓ valid response structure 265 | 266 | █ setup 267 | 268 | checks.........................: 97.66% ✓ 16233 ✗ 388 269 | data_received..................: 477 MB 7.6 MB/s 270 | data_sent......................: 6.7 MB 106 kB/s 271 | http_req_blocked...............: avg=4.28ms min=1.44µs med=2.95µs max=108.89ms p(90)=11.56µs p(95)=40.97ms 272 | http_req_connecting............: avg=4.18ms min=0s med=0s max=98.62ms p(90)=0s p(95)=40.43ms 273 | http_req_duration..............: avg=5.39s min=10.82ms med=3.01s max=1m0s p(90)=3.65s p(95)=8.33s 274 | { expected_response:true }...: avg=3.44s min=10.82ms med=2.99s max=59.48s p(90)=3.45s p(95)=4.15s 275 | http_req_failed................: 3.44% ✓ 194 ✗ 5431 276 | http_req_receiving.............: avg=443.64µs min=0s med=98.06µs max=184ms p(90)=240.5µs p(95)=535.19µs 277 | http_req_sending...............: avg=1.36ms min=8.69µs med=15.15µs max=41.76ms p(90)=135.51µs p(95)=18.04ms 278 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 279 | http_req_waiting...............: avg=5.39s min=10.74ms med=3.01s max=1m0s p(90)=3.65s p(95)=8.32s 280 | http_reqs......................: 5625 89.119332/s 281 | iteration_duration.............: avg=5.44s min=368.39ms med=3.03s max=1m0s p(90)=3.69s p(95)=8.83s 282 | iterations.....................: 5605 88.802464/s 283 | vus............................: 25 min=25 max=500 284 | vus_max........................: 500 min=500 max=500 285 | ``` 286 | 287 | 288 | **Performance Overview** 289 | 290 | 291 | Performance Overview 292 | 293 | 294 | **Subgraphs Overview** 295 | 296 | 297 | Subgraphs Overview 298 | 299 | 300 | **HTTP Overview** 301 | 302 | 303 | HTTP Overview 304 | 305 | 306 |
307 | 308 |
309 | Summary for: `hive-gateway` 310 | 311 | **K6 Output** 312 | 313 | 314 | 315 | 316 | ``` 317 | ✗ response code was 200 318 | ↳ 97% — ✓ 5592 / ✗ 123 319 | ✗ no graphql errors 320 | ↳ 97% — ✓ 5592 / ✗ 123 321 | ✓ valid response structure 322 | 323 | █ setup 324 | 325 | checks.........................: 98.55% ✓ 16776 ✗ 246 326 | data_received..................: 493 MB 7.7 MB/s 327 | data_sent......................: 6.8 MB 106 kB/s 328 | http_req_blocked...............: avg=2.91ms min=1.85µs med=3.95µs max=82.35ms p(90)=12.81µs p(95)=32.35ms 329 | http_req_connecting............: avg=2.85ms min=0s med=0s max=82.32ms p(90)=0s p(95)=32.05ms 330 | http_req_duration..............: avg=5.3s min=13.89ms med=3.08s max=1m0s p(90)=4.92s p(95)=18.64s 331 | { expected_response:true }...: avg=4.1s min=13.89ms med=3.04s max=59.85s p(90)=4.53s p(95)=6.22s 332 | http_req_failed................: 2.14% ✓ 123 ✗ 5612 333 | http_req_receiving.............: avg=1.8ms min=0s med=87.36µs max=312.25ms p(90)=645.64µs p(95)=3.05ms 334 | http_req_sending...............: avg=777.9µs min=8.21µs med=20.71µs max=132.7ms p(90)=428.38µs p(95)=3.16ms 335 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 336 | http_req_waiting...............: avg=5.3s min=13.7ms med=3.07s max=1m0s p(90)=4.92s p(95)=18.63s 337 | http_reqs......................: 5735 89.402296/s 338 | iteration_duration.............: avg=5.38s min=351.04ms med=3.15s max=1m0s p(90)=5.03s p(95)=18.78s 339 | iterations.....................: 5715 89.090518/s 340 | vus............................: 43 min=43 max=500 341 | vus_max........................: 500 min=500 max=500 342 | ``` 343 | 344 | 345 | **Performance Overview** 346 | 347 | 348 | Performance Overview 349 | 350 | 351 | **Subgraphs Overview** 352 | 353 | 354 | Subgraphs Overview 355 | 356 | 357 | **HTTP Overview** 358 | 359 | 360 | HTTP Overview 361 | 362 | 363 |
364 | 365 |
366 | Summary for: `mercurius` 367 | 368 | **K6 Output** 369 | 370 | 371 | 372 | 373 | ``` 374 | ✓ response code was 200 375 | ✓ no graphql errors 376 | ✓ valid response structure 377 | 378 | █ setup 379 | 380 | checks.........................: 100.00% ✓ 14340 ✗ 0 381 | data_received..................: 421 MB 6.7 MB/s 382 | data_sent......................: 5.7 MB 91 kB/s 383 | http_req_blocked...............: avg=4.51ms min=1.72µs med=3.67µs max=95.86ms p(90)=9.07ms p(95)=40.93ms 384 | http_req_connecting............: avg=4.4ms min=0s med=0s max=91.66ms p(90)=8.66ms p(95)=40.24ms 385 | http_req_duration..............: avg=6.35s min=11.42ms med=6.3s max=10.83s p(90)=7.63s p(95)=8.42s 386 | { expected_response:true }...: avg=6.35s min=11.42ms med=6.3s max=10.83s p(90)=7.63s p(95)=8.42s 387 | http_req_failed................: 0.00% ✓ 0 ✗ 4800 388 | http_req_receiving.............: avg=15.87ms min=37.74µs med=98.68µs max=1.22s p(90)=263.16µs p(95)=727.03µs 389 | http_req_sending...............: avg=1.73ms min=9.13µs med=21.36µs max=42.69ms p(90)=201.97µs p(95)=22.46ms 390 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 391 | http_req_waiting...............: avg=6.33s min=11.35ms med=6.3s max=10.83s p(90)=7.63s p(95)=8.42s 392 | http_reqs......................: 4800 76.38269/s 393 | iteration_duration.............: avg=6.41s min=438.97ms med=6.32s max=10.85s p(90)=7.65s p(95)=8.44s 394 | iterations.....................: 4780 76.064429/s 395 | vus............................: 271 min=271 max=500 396 | vus_max........................: 500 min=500 max=500 397 | ``` 398 | 399 | 400 | **Performance Overview** 401 | 402 | 403 | Performance Overview 404 | 405 | 406 | **Subgraphs Overview** 407 | 408 | 409 | Subgraphs Overview 410 | 411 | 412 | **HTTP Overview** 413 | 414 | 415 | HTTP Overview 416 | 417 | 418 |
-------------------------------------------------------------------------------- /federation/scenarios/constant-vus-subgraphs-delay/README.md: -------------------------------------------------------------------------------- 1 | ## Overview for: `federation/constant-vus-subgraphs-delay` 2 | 3 | 4 | This scenario runs 4 subgraphs and a GraphQL gateway with Federation spec, and runs a heavy query. It's being executed with a constant amount of VUs over a fixed amount of time. It measure things like memory usage, CPU usage, average RPS. It also includes a summary of the entire execution, and metrics information about HTTP execution times. 5 | 6 | 7 | This scenario was running 300 VUs over 60s 8 | 9 | 10 | ### Comparison 11 | 12 | 13 | Comparison 14 | 15 | 16 | | Gateway | RPS ⬇️ | Requests | Duration | Notes | 17 | | :--------------- | :----: | :-------------------: | :----------------------: | :----------------------------------------------------------------------- | 18 | | cosmo | 176 | 10760 total, 0 failed | avg: 821ms, p95: 2145ms | ✅ | 19 | | grafbase | 170 | 10387 total, 0 failed | avg: 836ms, p95: 2290ms | ✅ | 20 | | apollo-router | 158 | 9642 total, 0 failed | avg: 955ms, p95: 2575ms | ✅ | 21 | | hive-gateway-bun | 93 | 5900 total, 0 failed | avg: 3059ms, p95: 5013ms | ✅ | 22 | | hive-gateway | 88 | 5553 total, 4 failed | avg: 3285ms, p95: 4662ms | ❌ 4 failed requests, 4 non-200 responses, 4 unexpected GraphQL errors | 23 | | apollo-server | 87 | 5495 total, 68 failed | avg: 3314ms, p95: 3214ms | ❌ 68 failed requests, 68 non-200 responses, 68 unexpected GraphQL errors | 24 | | mercurius | 79 | 4915 total, 0 failed | avg: 3696ms, p95: 4823ms | ✅ | 25 | 26 | 27 | 28 |
29 | Summary for: `cosmo` 30 | 31 | **K6 Output** 32 | 33 | 34 | 35 | 36 | ``` 37 | ✓ response code was 200 38 | ✓ no graphql errors 39 | ✓ valid response structure 40 | 41 | █ setup 42 | 43 | checks.........................: 100.00% ✓ 32220 ✗ 0 44 | data_received..................: 944 MB 16 MB/s 45 | data_sent......................: 13 MB 210 kB/s 46 | http_req_blocked...............: avg=856.06µs min=1.66µs med=3.4µs max=1.36s p(90)=5.2µs p(95)=11.14µs 47 | http_req_connecting............: avg=505.37µs min=0s med=0s max=831.66ms p(90)=0s p(95)=0s 48 | http_req_duration..............: avg=821.08ms min=3.13ms med=665.36ms max=5.96s p(90)=1.7s p(95)=2.14s 49 | { expected_response:true }...: avg=821.08ms min=3.13ms med=665.36ms max=5.96s p(90)=1.7s p(95)=2.14s 50 | http_req_failed................: 0.00% ✓ 0 ✗ 10760 51 | http_req_receiving.............: avg=284.1ms min=33.64µs med=92.2µs max=5.28s p(90)=1.11s p(95)=1.61s 52 | http_req_sending...............: avg=23.12ms min=7.91µs med=15.83µs max=3.1s p(90)=119.99µs p(95)=5.77ms 53 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 54 | http_req_waiting...............: avg=513.85ms min=3.05ms med=466.43ms max=2.7s p(90)=944.34ms p(95)=1.1s 55 | http_reqs......................: 10760 176.461926/s 56 | iteration_duration.............: avg=1.67s min=25.93ms med=1.37s max=10.07s p(90)=3.51s p(95)=4.16s 57 | iterations.....................: 10740 176.13393/s 58 | vus............................: 2 min=2 max=300 59 | vus_max........................: 300 min=300 max=300 60 | ``` 61 | 62 | 63 | **Performance Overview** 64 | 65 | 66 | Performance Overview 67 | 68 | 69 | **Subgraphs Overview** 70 | 71 | 72 | Subgraphs Overview 73 | 74 | 75 | **HTTP Overview** 76 | 77 | 78 | HTTP Overview 79 | 80 | 81 |
82 | 83 |
84 | Summary for: `grafbase` 85 | 86 | **K6 Output** 87 | 88 | 89 | 90 | 91 | ``` 92 | ✓ response code was 200 93 | ✓ no graphql errors 94 | ✓ valid response structure 95 | 96 | █ setup 97 | 98 | checks.........................: 100.00% ✓ 31101 ✗ 0 99 | data_received..................: 913 MB 15 MB/s 100 | data_sent......................: 12 MB 202 kB/s 101 | http_req_blocked...............: avg=1.95ms min=1.36µs med=3.45µs max=2.78s p(90)=5.18µs p(95)=10.41µs 102 | http_req_connecting............: avg=1.62ms min=0s med=0s max=2.78s p(90)=0s p(95)=0s 103 | http_req_duration..............: avg=836.11ms min=3.01ms med=671.6ms max=4.66s p(90)=1.78s p(95)=2.28s 104 | { expected_response:true }...: avg=836.11ms min=3.01ms med=671.6ms max=4.66s p(90)=1.78s p(95)=2.28s 105 | http_req_failed................: 0.00% ✓ 0 ✗ 10387 106 | http_req_receiving.............: avg=277.57ms min=31.38µs med=88.31µs max=4.23s p(90)=1.17s p(95)=1.63s 107 | http_req_sending...............: avg=32.79ms min=7.87µs med=17.38µs max=3.46s p(90)=140.75µs p(95)=44.88ms 108 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 109 | http_req_waiting...............: avg=525.74ms min=2.94ms med=453.18ms max=3.35s p(90)=992.83ms p(95)=1.18s 110 | http_reqs......................: 10387 170.212373/s 111 | iteration_duration.............: avg=1.71s min=17.43ms med=1.48s max=8.65s p(90)=3.48s p(95)=4.15s 112 | iterations.....................: 10367 169.884632/s 113 | vus............................: 37 min=37 max=300 114 | vus_max........................: 300 min=300 max=300 115 | ``` 116 | 117 | 118 | **Performance Overview** 119 | 120 | 121 | Performance Overview 122 | 123 | 124 | **Subgraphs Overview** 125 | 126 | 127 | Subgraphs Overview 128 | 129 | 130 | **HTTP Overview** 131 | 132 | 133 | HTTP Overview 134 | 135 | 136 |
137 | 138 |
139 | Summary for: `apollo-router` 140 | 141 | **K6 Output** 142 | 143 | 144 | 145 | 146 | ``` 147 | ✓ response code was 200 148 | ✓ no graphql errors 149 | ✓ valid response structure 150 | 151 | █ setup 152 | 153 | checks.........................: 100.00% ✓ 28866 ✗ 0 154 | data_received..................: 846 MB 14 MB/s 155 | data_sent......................: 11 MB 189 kB/s 156 | http_req_blocked...............: avg=1.83ms min=1.48µs med=3.23µs max=1.79s p(90)=4.98µs p(95)=11.36µs 157 | http_req_connecting............: avg=1.54ms min=0s med=0s max=1.79s p(90)=0s p(95)=0s 158 | http_req_duration..............: avg=955.17ms min=6.38ms med=788.5ms max=6.5s p(90)=2.01s p(95)=2.57s 159 | { expected_response:true }...: avg=955.17ms min=6.38ms med=788.5ms max=6.5s p(90)=2.01s p(95)=2.57s 160 | http_req_failed................: 0.00% ✓ 0 ✗ 9642 161 | http_req_receiving.............: avg=324.59ms min=34.19µs med=85.25µs max=4.87s p(90)=1.31s p(95)=1.89s 162 | http_req_sending...............: avg=25.23ms min=7.61µs med=14.87µs max=3.33s p(90)=136.94µs p(95)=7.3ms 163 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 164 | http_req_waiting...............: avg=605.34ms min=6.32ms med=604.43ms max=2.29s p(90)=1.06s p(95)=1.23s 165 | http_reqs......................: 9642 158.776609/s 166 | iteration_duration.............: avg=1.87s min=33.21ms med=1.59s max=10.31s p(90)=3.75s p(95)=4.52s 167 | iterations.....................: 9622 158.447265/s 168 | vus............................: 300 min=300 max=300 169 | vus_max........................: 300 min=300 max=300 170 | ``` 171 | 172 | 173 | **Performance Overview** 174 | 175 | 176 | Performance Overview 177 | 178 | 179 | **Subgraphs Overview** 180 | 181 | 182 | Subgraphs Overview 183 | 184 | 185 | **HTTP Overview** 186 | 187 | 188 | HTTP Overview 189 | 190 | 191 |
192 | 193 |
194 | Summary for: `hive-gateway-bun` 195 | 196 | **K6 Output** 197 | 198 | 199 | 200 | 201 | ``` 202 | ✓ response code was 200 203 | ✓ no graphql errors 204 | ✓ valid response structure 205 | 206 | █ setup 207 | 208 | checks.........................: 100.00% ✓ 17640 ✗ 0 209 | data_received..................: 518 MB 8.2 MB/s 210 | data_sent......................: 7.0 MB 111 kB/s 211 | http_req_blocked...............: avg=492.66µs min=1.43µs med=3.6µs max=30.26ms p(90)=5.92µs p(95)=347.88µs 212 | http_req_connecting............: avg=467.75µs min=0s med=0s max=24.77ms p(90)=0s p(95)=142.71µs 213 | http_req_duration..............: avg=3.05s min=14.05ms med=2.8s max=6.96s p(90)=4.34s p(95)=5.01s 214 | { expected_response:true }...: avg=3.05s min=14.05ms med=2.8s max=6.96s p(90)=4.34s p(95)=5.01s 215 | http_req_failed................: 0.00% ✓ 0 ✗ 5900 216 | http_req_receiving.............: avg=41.63ms min=38.13µs med=147.83µs max=1.83s p(90)=3.84ms p(95)=242.75ms 217 | http_req_sending...............: avg=703.61µs min=9.27µs med=19.31µs max=465.59ms p(90)=59.75µs p(95)=478.33µs 218 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 219 | http_req_waiting...............: avg=3.01s min=13.84ms med=2.78s max=6.96s p(90)=4.28s p(95)=5s 220 | http_reqs......................: 5900 93.626406/s 221 | iteration_duration.............: avg=3.13s min=204.88ms med=2.84s max=7.05s p(90)=4.45s p(95)=5.1s 222 | iterations.....................: 5880 93.309028/s 223 | vus............................: 6 min=6 max=300 224 | vus_max........................: 300 min=300 max=300 225 | ``` 226 | 227 | 228 | **Performance Overview** 229 | 230 | 231 | Performance Overview 232 | 233 | 234 | **Subgraphs Overview** 235 | 236 | 237 | Subgraphs Overview 238 | 239 | 240 | **HTTP Overview** 241 | 242 | 243 | HTTP Overview 244 | 245 | 246 |
247 | 248 |
249 | Summary for: `hive-gateway` 250 | 251 | **K6 Output** 252 | 253 | 254 | 255 | 256 | ``` 257 | ✗ response code was 200 258 | ↳ 99% — ✓ 5529 / ✗ 4 259 | ✗ no graphql errors 260 | ↳ 99% — ✓ 5529 / ✗ 4 261 | ✓ valid response structure 262 | 263 | █ setup 264 | 265 | checks.........................: 99.95% ✓ 16587 ✗ 8 266 | data_received..................: 487 MB 7.7 MB/s 267 | data_sent......................: 6.6 MB 105 kB/s 268 | http_req_blocked...............: avg=817.13µs min=1.74µs med=3.62µs max=55.83ms p(90)=5.59µs p(95)=1.13ms 269 | http_req_connecting............: avg=792.83µs min=0s med=0s max=55.57ms p(90)=0s p(95)=663.66µs 270 | http_req_duration..............: avg=3.28s min=16.35ms med=2.39s max=1m0s p(90)=3.34s p(95)=4.66s 271 | { expected_response:true }...: avg=3.24s min=16.35ms med=2.39s max=59.94s p(90)=3.33s p(95)=4.64s 272 | http_req_failed................: 0.07% ✓ 4 ✗ 5549 273 | http_req_receiving.............: avg=894.65µs min=0s med=96.77µs max=199.39ms p(90)=500.61µs p(95)=1.47ms 274 | http_req_sending...............: avg=340.65µs min=8.77µs med=20.25µs max=119.65ms p(90)=70.71µs p(95)=511.01µs 275 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 276 | http_req_waiting...............: avg=3.28s min=16.23ms med=2.39s max=1m0s p(90)=3.34s p(95)=4.66s 277 | http_reqs......................: 5553 88.073494/s 278 | iteration_duration.............: avg=3.33s min=165.3ms med=2.42s max=1m0s p(90)=3.38s p(95)=4.75s 279 | iterations.....................: 5533 87.756283/s 280 | vus............................: 27 min=27 max=300 281 | vus_max........................: 300 min=300 max=300 282 | ``` 283 | 284 | 285 | **Performance Overview** 286 | 287 | 288 | Performance Overview 289 | 290 | 291 | **Subgraphs Overview** 292 | 293 | 294 | Subgraphs Overview 295 | 296 | 297 | **HTTP Overview** 298 | 299 | 300 | HTTP Overview 301 | 302 | 303 |
304 | 305 |
306 | Summary for: `apollo-server` 307 | 308 | **K6 Output** 309 | 310 | 311 | 312 | 313 | ``` 314 | ✗ response code was 200 315 | ↳ 98% — ✓ 5407 / ✗ 68 316 | ✗ no graphql errors 317 | ↳ 98% — ✓ 5407 / ✗ 68 318 | ✓ valid response structure 319 | 320 | █ setup 321 | 322 | checks.........................: 99.16% ✓ 16221 ✗ 136 323 | data_received..................: 477 MB 7.6 MB/s 324 | data_sent......................: 6.5 MB 104 kB/s 325 | http_req_blocked...............: avg=279.03µs min=1.5µs med=3.42µs max=26.26ms p(90)=5.59µs p(95)=248.44µs 326 | http_req_connecting............: avg=263.09µs min=0s med=0s max=13.95ms p(90)=0s p(95)=209.1µs 327 | http_req_duration..............: avg=3.31s min=11.06ms med=1.92s max=1m0s p(90)=2.6s p(95)=3.21s 328 | { expected_response:true }...: avg=2.6s min=11.06ms med=1.91s max=59.91s p(90)=2.55s p(95)=2.85s 329 | http_req_failed................: 1.23% ✓ 68 ✗ 5427 330 | http_req_receiving.............: avg=217.13µs min=0s med=108.46µs max=54.26ms p(90)=205.8µs p(95)=319.9µs 331 | http_req_sending...............: avg=143.59µs min=8.72µs med=17.98µs max=35.37ms p(90)=39.36µs p(95)=209.97µs 332 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 333 | http_req_waiting...............: avg=3.31s min=10.97ms med=1.92s max=1m0s p(90)=2.6s p(95)=3.21s 334 | http_reqs......................: 5495 87.862734/s 335 | iteration_duration.............: avg=3.34s min=298.92ms med=1.94s max=1m0s p(90)=2.62s p(95)=3.24s 336 | iterations.....................: 5475 87.542943/s 337 | vus............................: 93 min=93 max=300 338 | vus_max........................: 300 min=300 max=300 339 | ``` 340 | 341 | 342 | **Performance Overview** 343 | 344 | 345 | Performance Overview 346 | 347 | 348 | **Subgraphs Overview** 349 | 350 | 351 | Subgraphs Overview 352 | 353 | 354 | **HTTP Overview** 355 | 356 | 357 | HTTP Overview 358 | 359 | 360 |
361 | 362 |
363 | Summary for: `mercurius` 364 | 365 | **K6 Output** 366 | 367 | 368 | 369 | 370 | ``` 371 | ✓ response code was 200 372 | ✓ no graphql errors 373 | ✓ valid response structure 374 | 375 | █ setup 376 | 377 | checks.........................: 100.00% ✓ 14685 ✗ 0 378 | data_received..................: 431 MB 7.0 MB/s 379 | data_sent......................: 5.8 MB 94 kB/s 380 | http_req_blocked...............: avg=899.3µs min=1.41µs med=3.64µs max=59.09ms p(90)=5.64µs p(95)=1.35ms 381 | http_req_connecting............: avg=861.52µs min=0s med=0s max=48.45ms p(90)=0s p(95)=1.17ms 382 | http_req_duration..............: avg=3.69s min=11.96ms med=3.66s max=8.6s p(90)=4.07s p(95)=4.82s 383 | { expected_response:true }...: avg=3.69s min=11.96ms med=3.66s max=8.6s p(90)=4.07s p(95)=4.82s 384 | http_req_failed................: 0.00% ✓ 0 ✗ 4915 385 | http_req_receiving.............: avg=10.08ms min=40.77µs med=104.43µs max=1.08s p(90)=337.54µs p(95)=1.12ms 386 | http_req_sending...............: avg=330.98µs min=9.38µs med=20.28µs max=118.33ms p(90)=45.19µs p(95)=684.08µs 387 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 388 | http_req_waiting...............: avg=3.68s min=11.83ms med=3.65s max=8.6s p(90)=4.07s p(95)=4.82s 389 | http_reqs......................: 4915 79.282525/s 390 | iteration_duration.............: avg=3.73s min=144.42ms med=3.69s max=8.67s p(90)=4.11s p(95)=4.85s 391 | iterations.....................: 4895 78.95991/s 392 | vus............................: 11 min=11 max=300 393 | vus_max........................: 300 min=300 max=300 394 | ``` 395 | 396 | 397 | **Performance Overview** 398 | 399 | 400 | Performance Overview 401 | 402 | 403 | **Subgraphs Overview** 404 | 405 | 406 | Subgraphs Overview 407 | 408 | 409 | **HTTP Overview** 410 | 411 | 412 | HTTP Overview 413 | 414 | 415 |
-------------------------------------------------------------------------------- /federation/scenarios/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "federation-scenarios", 3 | "scripts": { 4 | "compose-supergraph": "tsx compose-supergraph.ts" 5 | }, 6 | "dependencies": { 7 | "tsx": "4.19.4", 8 | "typescript": "5.8.3", 9 | "graphql": "16.11.0", 10 | "@apollo/composition": "2.10.2" 11 | }, 12 | "devDependencies": { 13 | "@types/node": "22.15.24" 14 | }, 15 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 16 | } 17 | -------------------------------------------------------------------------------- /federation/scenarios/ramping-vus/README.md: -------------------------------------------------------------------------------- 1 | ## Overview for: `federation/ramping-vus` 2 | 3 | 4 | This scenario runs 4 subgraphs and a GraphQL gateway with Federation spec, and runs a heavy query. We are running a heavy load of concurrent VUs to measure response time and other stats, during stress. It measure things like memory usage, CPU usage, response times. It also includes a summary of the entire execution, and metrics information about HTTP execution times. 5 | 6 | 7 | This scenario was trying to reach 2000 concurrent VUs over 60s 8 | 9 | 10 | ### Comparison 11 | 12 | 13 | Comparison 14 | 15 | 16 | | Gateway | duration(p95)⬇️ | RPS | Requests | Durations | Notes | 17 | | :--------------- | :-------------: | :---: | :---------------------: | :----------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------- | 18 | | cosmo | 5255ms | 175 | 12336 total, 0 failed | avg: 2460ms, p95: 5255ms, max: 10888ms, med: 2270ms | ✅ | 19 | | grafbase | 6605ms | 157 | 11315 total, 0 failed | avg: 2875ms, p95: 6606ms, max: 12606ms, med: 2506ms | ✅ | 20 | | apollo-router | 13418ms | 160 | 12877 total, 381 failed | avg: 3435ms, p95: 13418ms, max: 27087ms, med: 2301ms | ❌ 381 failed requests, 381 non-200 responses, 382 unexpected GraphQL errors, non-compatible response structure (1) | 21 | | hive-gateway-bun | 24595ms | 91 | 7363 total, 0 failed | avg: 11307ms, p95: 24595ms, max: 33434ms, med: 9249ms | ✅ | 22 | | mercurius | 42215ms | 53 | 4896 total, 0 failed | avg: 21742ms, p95: 42215ms, max: 43072ms, med: 20027ms | ✅ | 23 | | hive-gateway | 51561ms | 85 | 7544 total, 0 failed | avg: 12296ms, p95: 51561ms, max: 58106ms, med: 3374ms | ✅ | 24 | | apollo-server | 59999ms | 82 | 7676 total, 504 failed | avg: 11947ms, p95: 60000ms, max: 60112ms, med: 2188ms | ❌ 504 failed requests, 504 non-200 responses, 504 unexpected GraphQL errors | 25 | 26 | 27 | 28 |
29 | Summary for: `cosmo` 30 | 31 | **K6 Output** 32 | 33 | 34 | 35 | 36 | ``` 37 | ✓ response code was 200 38 | ✓ no graphql errors 39 | ✓ valid response structure 40 | 41 | █ setup 42 | 43 | checks.........................: 100.00% ✓ 36948 ✗ 0 44 | data_received..................: 1.1 GB 15 MB/s 45 | data_sent......................: 15 MB 209 kB/s 46 | http_req_blocked...............: avg=254.97ms min=1.47µs med=3.6µs max=8s p(90)=673.66ms p(95)=2.07s 47 | http_req_connecting............: avg=251.78ms min=0s med=0s max=8s p(90)=642.86ms p(95)=2.06s 48 | http_req_duration..............: avg=2.46s min=3.16ms med=2.26s max=10.88s p(90)=4.62s p(95)=5.25s 49 | { expected_response:true }...: avg=2.46s min=3.16ms med=2.26s max=10.88s p(90)=4.62s p(95)=5.25s 50 | http_req_failed................: 0.00% ✓ 0 ✗ 12336 51 | http_req_receiving.............: avg=109.29ms min=32.96µs med=76.88µs max=7.09s p(90)=245.82ms p(95)=676.04ms 52 | http_req_sending...............: avg=129.34ms min=7.62µs med=17.62µs max=6.9s p(90)=298.96ms p(95)=931.43ms 53 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 54 | http_req_waiting...............: avg=2.22s min=2.99ms med=1.92s max=10.88s p(90)=4.41s p(95)=5.01s 55 | http_reqs......................: 12336 175.989356/s 56 | iteration_duration.............: avg=5.57s min=9.09ms med=4.93s max=29.17s p(90)=11.15s p(95)=12.7s 57 | iterations.....................: 12316 175.70403/s 58 | vus............................: 4 min=4 max=1926 59 | vus_max........................: 2000 min=2000 max=2000 60 | ``` 61 | 62 | 63 | **Performance Overview** 64 | 65 | 66 | Performance Overview 67 | 68 | 69 | **Subgraphs Overview** 70 | 71 | 72 | Subgraphs Overview 73 | 74 | 75 | **HTTP Overview** 76 | 77 | 78 | HTTP Overview 79 | 80 | 81 |
82 | 83 |
84 | Summary for: `grafbase` 85 | 86 | **K6 Output** 87 | 88 | 89 | 90 | 91 | ``` 92 | ✓ response code was 200 93 | ✓ no graphql errors 94 | ✓ valid response structure 95 | 96 | █ setup 97 | 98 | checks.........................: 100.00% ✓ 33885 ✗ 0 99 | data_received..................: 994 MB 14 MB/s 100 | data_sent......................: 13 MB 187 kB/s 101 | http_req_blocked...............: avg=270.72ms min=1.7µs med=4.73µs max=10.92s p(90)=881.66ms p(95)=2.22s 102 | http_req_connecting............: avg=267.82ms min=0s med=0s max=10.92s p(90)=867.01ms p(95)=2.22s 103 | http_req_duration..............: avg=2.87s min=3.3ms med=2.5s max=12.6s p(90)=5.57s p(95)=6.6s 104 | { expected_response:true }...: avg=2.87s min=3.3ms med=2.5s max=12.6s p(90)=5.57s p(95)=6.6s 105 | http_req_failed................: 0.00% ✓ 0 ✗ 11315 106 | http_req_receiving.............: avg=421.51ms min=32.69µs med=96.14µs max=9.76s p(90)=1.45s p(95)=3.04s 107 | http_req_sending...............: avg=140.46ms min=8.58µs med=25.68µs max=6.37s p(90)=265.41ms p(95)=831.91ms 108 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 109 | http_req_waiting...............: avg=2.31s min=3.17ms med=2s max=10.43s p(90)=4.69s p(95)=5.26s 110 | http_reqs......................: 11315 157.883844/s 111 | iteration_duration.............: avg=6.43s min=24.18ms med=5.49s max=31.54s p(90)=13.32s p(95)=15.73s 112 | iterations.....................: 11295 157.604774/s 113 | vus............................: 377 min=71 max=1780 114 | vus_max........................: 2000 min=2000 max=2000 115 | ``` 116 | 117 | 118 | **Performance Overview** 119 | 120 | 121 | Performance Overview 122 | 123 | 124 | **Subgraphs Overview** 125 | 126 | 127 | Subgraphs Overview 128 | 129 | 130 | **HTTP Overview** 131 | 132 | 133 | HTTP Overview 134 | 135 | 136 |
137 | 138 |
139 | Summary for: `apollo-router` 140 | 141 | **K6 Output** 142 | 143 | 144 | 145 | 146 | ``` 147 | ✗ response code was 200 148 | ↳ 97% — ✓ 12476 / ✗ 381 149 | ✗ no graphql errors 150 | ↳ 97% — ✓ 12475 / ✗ 382 151 | ✗ valid response structure 152 | ↳ 99% — ✓ 12475 / ✗ 1 153 | 154 | █ setup 155 | 156 | checks.........................: 97.99% ✓ 37426 ✗ 764 157 | data_received..................: 1.1 GB 14 MB/s 158 | data_sent......................: 15 MB 191 kB/s 159 | http_req_blocked...............: avg=641.88ms min=1.6µs med=4.15µs max=21.98s p(90)=2.04s p(95)=3.8s 160 | http_req_connecting............: avg=620.14ms min=0s med=0s max=21.98s p(90)=2s p(95)=3.79s 161 | http_req_duration..............: avg=3.43s min=0s med=2.3s max=27.08s p(90)=6.64s p(95)=13.41s 162 | { expected_response:true }...: avg=3.4s min=6.52ms med=2.29s max=27.08s p(90)=6.46s p(95)=13.35s 163 | http_req_failed................: 2.95% ✓ 381 ✗ 12496 164 | http_req_receiving.............: avg=994.18ms min=0s med=88.47µs max=24.84s p(90)=2.45s p(95)=8.85s 165 | http_req_sending...............: avg=349.64ms min=0s med=22.68µs max=20.79s p(90)=516.77ms p(95)=1.07s 166 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 167 | http_req_waiting...............: avg=2.09s min=0s med=1.62s max=20.69s p(90)=4.23s p(95)=5.15s 168 | http_reqs......................: 12877 160.556988/s 169 | iteration_duration.............: avg=7.22s min=74.59ms med=5.33s max=38.35s p(90)=17.48s p(95)=21.07s 170 | iterations.....................: 12857 160.307618/s 171 | vus............................: 209 min=69 max=1927 172 | vus_max........................: 2000 min=2000 max=2000 173 | ``` 174 | 175 | 176 | **Performance Overview** 177 | 178 | 179 | Performance Overview 180 | 181 | 182 | **Subgraphs Overview** 183 | 184 | 185 | Subgraphs Overview 186 | 187 | 188 | **HTTP Overview** 189 | 190 | 191 | HTTP Overview 192 | 193 | 194 |
195 | 196 |
197 | Summary for: `hive-gateway-bun` 198 | 199 | **K6 Output** 200 | 201 | 202 | 203 | 204 | ``` 205 | ✓ response code was 200 206 | ✓ no graphql errors 207 | ✓ valid response structure 208 | 209 | █ setup 210 | 211 | checks.........................: 100.00% ✓ 22029 ✗ 0 212 | data_received..................: 646 MB 8.0 MB/s 213 | data_sent......................: 8.7 MB 109 kB/s 214 | http_req_blocked...............: avg=36.08ms min=1.49µs med=4.61µs max=1.85s p(90)=55.39ms p(95)=239.76ms 215 | http_req_connecting............: avg=35.52ms min=0s med=0s max=1.85s p(90)=54.07ms p(95)=236.46ms 216 | http_req_duration..............: avg=11.3s min=15.56ms med=9.24s max=33.43s p(90)=22.28s p(95)=24.59s 217 | { expected_response:true }...: avg=11.3s min=15.56ms med=9.24s max=33.43s p(90)=22.28s p(95)=24.59s 218 | http_req_failed................: 0.00% ✓ 0 ✗ 7363 219 | http_req_receiving.............: avg=261.89ms min=36.2µs med=120.75µs max=7.78s p(90)=596.94ms p(95)=1.62s 220 | http_req_sending...............: avg=27.41ms min=9.79µs med=28.55µs max=3.68s p(90)=36.19ms p(95)=121.81ms 221 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 222 | http_req_waiting...............: avg=11.01s min=15.42ms med=8.92s max=31.94s p(90)=21.7s p(95)=24.44s 223 | http_reqs......................: 7363 91.547735/s 224 | iteration_duration.............: avg=11.96s min=142.49ms med=9.99s max=36.65s p(90)=23.86s p(95)=26.31s 225 | iterations.....................: 7343 91.299066/s 226 | vus............................: 298 min=59 max=1979 227 | vus_max........................: 2000 min=2000 max=2000 228 | ``` 229 | 230 | 231 | **Performance Overview** 232 | 233 | 234 | Performance Overview 235 | 236 | 237 | **Subgraphs Overview** 238 | 239 | 240 | Subgraphs Overview 241 | 242 | 243 | **HTTP Overview** 244 | 245 | 246 | HTTP Overview 247 | 248 | 249 |
250 | 251 |
252 | Summary for: `mercurius` 253 | 254 | **K6 Output** 255 | 256 | 257 | 258 | 259 | ``` 260 | ✓ response code was 200 261 | ✓ no graphql errors 262 | ✓ valid response structure 263 | 264 | █ setup 265 | 266 | checks.........................: 100.00% ✓ 14588 ✗ 0 267 | data_received..................: 430 MB 4.7 MB/s 268 | data_sent......................: 6.0 MB 65 kB/s 269 | http_req_blocked...............: avg=271.78µs min=1.7µs med=4.59µs max=31.24ms p(90)=605.78µs p(95)=1.03ms 270 | http_req_connecting............: avg=236.75µs min=0s med=0s max=31.02ms p(90)=524.72µs p(95)=880.51µs 271 | http_req_duration..............: avg=21.74s min=10.88ms med=20.02s max=43.07s p(90)=41.12s p(95)=42.21s 272 | { expected_response:true }...: avg=21.74s min=10.88ms med=20.02s max=43.07s p(90)=41.12s p(95)=42.21s 273 | http_req_failed................: 0.00% ✓ 0 ✗ 4896 274 | http_req_receiving.............: avg=27.04ms min=38.07µs med=108.89µs max=2.39s p(90)=539.23µs p(95)=2.14ms 275 | http_req_sending...............: avg=64.71µs min=8.54µs med=28.75µs max=18.5ms p(90)=73.18µs p(95)=106.34µs 276 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 277 | http_req_waiting...............: avg=21.71s min=10.76ms med=20.02s max=43.07s p(90)=40.72s p(95)=42.2s 278 | http_reqs......................: 4896 53.18749/s 279 | iteration_duration.............: avg=21.84s min=222.77ms med=19.99s max=45.03s p(90)=41.64s p(95)=42.35s 280 | iterations.....................: 4836 52.535682/s 281 | vus............................: 271 min=59 max=2000 282 | vus_max........................: 2000 min=2000 max=2000 283 | ``` 284 | 285 | 286 | **Performance Overview** 287 | 288 | 289 | Performance Overview 290 | 291 | 292 | **Subgraphs Overview** 293 | 294 | 295 | Subgraphs Overview 296 | 297 | 298 | **HTTP Overview** 299 | 300 | 301 | HTTP Overview 302 | 303 | 304 |
305 | 306 |
307 | Summary for: `hive-gateway` 308 | 309 | **K6 Output** 310 | 311 | 312 | 313 | 314 | ``` 315 | ✓ response code was 200 316 | ✓ no graphql errors 317 | ✓ valid response structure 318 | 319 | █ setup 320 | 321 | checks.........................: 100.00% ✓ 22572 ✗ 0 322 | data_received..................: 663 MB 7.5 MB/s 323 | data_sent......................: 9.0 MB 101 kB/s 324 | http_req_blocked...............: avg=4.32ms min=1.59µs med=4.67µs max=249.86ms p(90)=5.87ms p(95)=31.04ms 325 | http_req_connecting............: avg=4.24ms min=0s med=0s max=249.79ms p(90)=5.57ms p(95)=30.77ms 326 | http_req_duration..............: avg=12.29s min=13.81ms med=3.37s max=58.1s p(90)=44.82s p(95)=51.56s 327 | { expected_response:true }...: avg=12.29s min=13.81ms med=3.37s max=58.1s p(90)=44.82s p(95)=51.56s 328 | http_req_failed................: 0.00% ✓ 0 ✗ 7544 329 | http_req_receiving.............: avg=1.79ms min=42.37µs med=104.63µs max=464.71ms p(90)=917.44µs p(95)=2.91ms 330 | http_req_sending...............: avg=1.7ms min=8.74µs med=27.68µs max=162.15ms p(90)=710.63µs p(95)=12.14ms 331 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 332 | http_req_waiting...............: avg=12.29s min=13.7ms med=3.37s max=58.1s p(90)=44.82s p(95)=51.56s 333 | http_reqs......................: 7544 85.421201/s 334 | iteration_duration.............: avg=12.38s min=128.41ms med=3.43s max=58.15s p(90)=44.91s p(95)=51.63s 335 | iterations.....................: 7524 85.19474/s 336 | vus............................: 52 min=52 max=2000 337 | vus_max........................: 2000 min=2000 max=2000 338 | ``` 339 | 340 | 341 | **Performance Overview** 342 | 343 | 344 | Performance Overview 345 | 346 | 347 | **Subgraphs Overview** 348 | 349 | 350 | Subgraphs Overview 351 | 352 | 353 | **HTTP Overview** 354 | 355 | 356 | HTTP Overview 357 | 358 | 359 |
360 | 361 |
362 | Summary for: `apollo-server` 363 | 364 | **K6 Output** 365 | 366 | 367 | 368 | 369 | ``` 370 | ✗ response code was 200 371 | ↳ 93% — ✓ 7152 / ✗ 504 372 | ✗ no graphql errors 373 | ↳ 93% — ✓ 7152 / ✗ 504 374 | ✓ valid response structure 375 | 376 | █ setup 377 | 378 | checks.........................: 95.51% ✓ 21456 ✗ 1008 379 | data_received..................: 630 MB 6.8 MB/s 380 | data_sent......................: 9.2 MB 99 kB/s 381 | http_req_blocked...............: avg=585µs min=1.39µs med=3.44µs max=100.62ms p(90)=429.75µs p(95)=816.06µs 382 | http_req_connecting............: avg=556.56µs min=0s med=0s max=100.55ms p(90)=356.8µs p(95)=712.42µs 383 | http_req_duration..............: avg=11.94s min=10.99ms med=2.18s max=1m0s p(90)=55.63s p(95)=59.99s 384 | { expected_response:true }...: avg=8.58s min=10.99ms med=2.11s max=59.99s p(90)=37.34s p(95)=47.86s 385 | http_req_failed................: 6.56% ✓ 504 ✗ 7172 386 | http_req_receiving.............: avg=218.11µs min=0s med=106.47µs max=195.2ms p(90)=231.81µs p(95)=367.6µs 387 | http_req_sending...............: avg=252.54µs min=8.32µs med=17.96µs max=60.01ms p(90)=69.72µs p(95)=145.26µs 388 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 389 | http_req_waiting...............: avg=11.94s min=10.9ms med=2.18s max=1m0s p(90)=55.63s p(95)=59.99s 390 | http_reqs......................: 7676 82.82067/s 391 | iteration_duration.............: avg=11.99s min=61.11ms med=2.2s max=1m0s p(90)=55.66s p(95)=1m0s 392 | iterations.....................: 7656 82.604879/s 393 | vus............................: 78 min=62 max=2000 394 | vus_max........................: 2000 min=2000 max=2000 395 | ``` 396 | 397 | 398 | **Performance Overview** 399 | 400 | 401 | Performance Overview 402 | 403 | 404 | **Subgraphs Overview** 405 | 406 | 407 | Subgraphs Overview 408 | 409 | 410 | **HTTP Overview** 411 | 412 | 413 | HTTP Overview 414 | 415 | 416 |
-------------------------------------------------------------------------------- /federation/scenarios/ramping-vus/benchmark.k6.js: -------------------------------------------------------------------------------- 1 | import { makeGraphQLRequest, handleBenchmarkSummary, sendGraphQLRequest } from '../k6.shared.js' 2 | 3 | const vus = __ENV.BENCH_VUS ? parseInt(__ENV.BENCH_VUS) : 500; 4 | const time = __ENV.BENCH_OVER_TIME || "30s"; 5 | 6 | export const options = { 7 | scenarios: { 8 | test: { 9 | executor: "ramping-vus", 10 | startVUs: 50, 11 | stages: [ 12 | { 13 | duration: time, 14 | target: vus, 15 | }, 16 | { duration: "5s", target: 50 }, 17 | { duration: "5s", target: 0 }, 18 | ], 19 | }, 20 | }, 21 | }; 22 | 23 | export function setup() { 24 | for (let i = 0; i < 20; i++) { 25 | sendGraphQLRequest(); 26 | } 27 | } 28 | 29 | export default function() { 30 | makeGraphQLRequest() 31 | } 32 | 33 | export function handleSummary(data) { 34 | return handleBenchmarkSummary(data, { vus, time }); 35 | } -------------------------------------------------------------------------------- /federation/scenarios/ramping-vus/generate-report.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, readdirSync, writeFileSync } from "fs"; 2 | import { join } from "path"; 3 | import pkgJson from "./package.json"; 4 | import tablemark from "tablemark"; 5 | import * as vl from "vega-lite"; 6 | import * as v from "vega"; 7 | 8 | const IGNORED_DIRS = ["node_modules", "services"]; 9 | const NEWLINE = "\n"; 10 | 11 | const { 12 | CF_IMAGES_LINK, 13 | CF_IMAGES_TOKEN, 14 | GITHUB_RUN_ID = "local", 15 | } = process.env; 16 | 17 | async function uploadImageToCloudflare(filename: string, filePath: string) { 18 | console.log("Uploading image to cloudflare"); 19 | const buffer = readFileSync(filePath); 20 | const blob = new Blob([buffer], { type: "image/png" }); 21 | const form = new FormData(); 22 | 23 | form.append("file", blob, filename); 24 | 25 | const res = await fetch(CF_IMAGES_LINK!, { 26 | method: "POST", 27 | body: form, 28 | headers: { 29 | Authorization: `Bearer ${CF_IMAGES_TOKEN}`, 30 | }, 31 | }); 32 | 33 | console.log(`Got a response from cloudflare (status=${res.status})`); 34 | 35 | if (!res.ok) { 36 | throw new Error(`Failed to upload image to Cloudflare: ${res.statusText}`); 37 | } 38 | 39 | const data = await res.json(); 40 | return data.result.variants[0]; 41 | } 42 | 43 | function notEmpty(value: TValue | null | undefined): value is TValue { 44 | return value !== null && value !== undefined; 45 | } 46 | 47 | function formatSummary(title: string, content: string) { 48 | return `
49 | ${title} 50 | 51 | ${content} 52 |
`; 53 | } 54 | 55 | async function generateReport(artifactsRootPath: string) { 56 | console.info(`Generating report based on artifacts in ${artifactsRootPath}`); 57 | const foundDirectories = readdirSync(artifactsRootPath, { 58 | withFileTypes: true, 59 | }) 60 | .filter((r) => r.isDirectory() && !IGNORED_DIRS.includes(r.name)) 61 | .filter((r) => r.name.startsWith(process.env.SCENARIO_ARTIFACTS_PREFIX!)) 62 | .map((r) => r.name); 63 | console.info( 64 | `Found the following directories to look reports in: ${foundDirectories.join( 65 | ", " 66 | )}` 67 | ); 68 | 69 | if (foundDirectories.length === 0) { 70 | throw new Error("No directories found to generate report from!"); 71 | } 72 | 73 | const reportsData = await Promise.all( 74 | foundDirectories.map(async (dirName) => { 75 | const fullPath = join(artifactsRootPath, dirName); 76 | const jsonSummaryFilePath = join(fullPath, "./k6_summary.json"); 77 | 78 | if (!existsSync(jsonSummaryFilePath)) { 79 | console.warn( 80 | `Could not find k6_summary.json in ${fullPath}! Skipping...` 81 | ); 82 | 83 | return null; 84 | } 85 | 86 | const txtSummaryFilePath = join(fullPath, "./k6_summary.txt"); 87 | 88 | let overviewImageUrl = ""; 89 | let httpImageUrl = ""; 90 | let containersImageUrl = ""; 91 | 92 | if (!CF_IMAGES_LINK || !CF_IMAGES_TOKEN) { 93 | console.warn( 94 | `Could not find CF_IMAGES_LINK or CF_IMAGES_TOKEN in env! Skipping...` 95 | ); 96 | } else { 97 | const overviewImageFilePath = join(fullPath, "./overview.png"); 98 | const httpImageFilePath = join(fullPath, "./http.png"); 99 | const containersFilePath = join(fullPath, "./containers.png"); 100 | [overviewImageUrl, httpImageUrl, containersImageUrl] = 101 | await Promise.all([ 102 | uploadImageToCloudflare( 103 | `${GITHUB_RUN_ID}-overview.png`, 104 | overviewImageFilePath 105 | ), 106 | uploadImageToCloudflare( 107 | `${GITHUB_RUN_ID}-http.png`, 108 | httpImageFilePath 109 | ), 110 | uploadImageToCloudflare( 111 | `${GITHUB_RUN_ID}-http.png`, 112 | containersFilePath 113 | ), 114 | ]); 115 | } 116 | 117 | const jsonSummary = JSON.parse(readFileSync(jsonSummaryFilePath, "utf8")); 118 | 119 | return { 120 | name: dirName.replace(process.env.SCENARIO_ARTIFACTS_PREFIX!, ""), 121 | path: fullPath, 122 | jsonSummary, 123 | txtSummary: readFileSync(txtSummaryFilePath, "utf8"), 124 | rps: Math.floor(jsonSummary.metrics.http_reqs.values.rate), 125 | p95_duration: Math.floor( 126 | jsonSummary.metrics.http_req_duration.values["p(95)"] 127 | ), 128 | overviewImageUrl, 129 | httpImageUrl, 130 | containersImageUrl, 131 | vus: jsonSummary.vus, 132 | time: jsonSummary.time, 133 | }; 134 | }) 135 | ); 136 | 137 | const validReportsData = reportsData 138 | .filter(notEmpty) 139 | .sort((a, b) => a.p95_duration - b.p95_duration); 140 | 141 | const vega: vl.TopLevelSpec = { 142 | width: 600, 143 | height: 400, 144 | background: null as any, 145 | $schema: "https://vega.github.io/schema/vega-lite/v5.json", 146 | description: "", 147 | data: { 148 | values: validReportsData.map((v) => { 149 | return { 150 | "gateway-setup": v.name, 151 | "duration (p95)": v.p95_duration, 152 | }; 153 | }), 154 | }, 155 | mark: "bar", 156 | encoding: { 157 | x: { 158 | field: "gateway-setup", 159 | type: "nominal", 160 | sort: "-y", 161 | }, 162 | y: { 163 | field: "duration (p95)", 164 | type: "quantitative", 165 | }, 166 | }, 167 | }; 168 | 169 | const vegaSpec = vl.compile(vega).spec; 170 | const view = new v.View(v.parse(vegaSpec), { renderer: "none" }); 171 | const svg = await view.toSVG(); 172 | writeFileSync("report.svg", svg); 173 | 174 | const reportChartUrl = await uploadImageToCloudflare( 175 | `${GITHUB_RUN_ID}-report.svg`, 176 | "report.svg" 177 | ); 178 | 179 | const markdownLines: string[] = [ 180 | "## Overview for: `federation/ramping-vus`", 181 | NEWLINE, 182 | pkgJson.description, 183 | NEWLINE, 184 | `This scenario was trying to reach ${validReportsData[0].vus} concurrent VUs over ${validReportsData[0].time}`, 185 | NEWLINE, 186 | "### Comparison", 187 | NEWLINE, 188 | reportChartUrl 189 | ? `Comparison` 190 | : "**no-chart-available**", 191 | NEWLINE, 192 | tablemark( 193 | validReportsData.map((v) => { 194 | const notes: string[] = []; 195 | 196 | if (v.jsonSummary.metrics.http_req_failed.values.passes > 0) { 197 | notes.push( 198 | `${v.jsonSummary.metrics.http_req_failed.values.passes} failed requests` 199 | ); 200 | } 201 | 202 | const checks = v.jsonSummary.root_group.checks; 203 | const http200Check = checks.find( 204 | (c) => c.name === "response code was 200" 205 | ); 206 | const graphqlErrors = checks.find( 207 | (c) => c.name === "no graphql errors" 208 | ); 209 | const responseStructure = checks.find( 210 | (c) => c.name === "valid response structure" 211 | ); 212 | 213 | if (http200Check.fails > 0) { 214 | notes.push(`${http200Check.fails} non-200 responses`); 215 | } 216 | 217 | if (graphqlErrors.fails > 0) { 218 | notes.push(`${graphqlErrors.fails} unexpected GraphQL errors`); 219 | } 220 | 221 | if (responseStructure.fails > 0) { 222 | notes.push( 223 | `non-compatible response structure (${responseStructure.fails})` 224 | ); 225 | } 226 | 227 | return { 228 | gw: v.name, 229 | p95_duration: `${v.p95_duration}ms`, 230 | rps: Math.round(v.rps), 231 | requests: `${v.jsonSummary.metrics.http_reqs.values.count} total, ${v.jsonSummary.metrics.http_req_failed.values.passes} failed`, 232 | duration: `avg: ${Math.round( 233 | v.jsonSummary.metrics.http_req_duration.values.avg 234 | )}ms, p95: ${Math.round( 235 | v.jsonSummary.metrics.http_req_duration.values["p(95)"] 236 | )}ms, max: ${Math.round( 237 | v.jsonSummary.metrics.http_req_duration.values.max 238 | )}ms, med: ${Math.round( 239 | v.jsonSummary.metrics.http_req_duration.values.med 240 | )}ms`, 241 | notes: notes.length === 0 ? "✅" : "❌ " + notes.join(", "), 242 | }; 243 | }), 244 | { 245 | columns: [ 246 | { name: "Gateway" }, 247 | { name: "duration(p95)⬇️", align: "center" }, 248 | { name: "RPS", align: "center" }, 249 | { name: "Requests", align: "center" }, 250 | { name: "Durations", align: "center" }, 251 | { name: "Notes", align: "left" }, 252 | ], 253 | } 254 | ), 255 | NEWLINE, 256 | validReportsData 257 | .map((info) => 258 | formatSummary( 259 | `Summary for: \`${info.name}\``, 260 | [ 261 | "**K6 Output**", 262 | NEWLINE, 263 | NEWLINE, 264 | "```", 265 | info.txtSummary, 266 | "```", 267 | NEWLINE, 268 | "**Performance Overview**", 269 | NEWLINE, 270 | info.overviewImageUrl 271 | ? `Performance Overview` 272 | : "**no-image-available**", 273 | NEWLINE, 274 | "**Subgraphs Overview**", 275 | NEWLINE, 276 | info.containersImageUrl 277 | ? `Subgraphs Overview` 278 | : "**no-image-available**", 279 | NEWLINE, 280 | "**HTTP Overview**", 281 | NEWLINE, 282 | info.httpImageUrl 283 | ? `HTTP Overview` 284 | : "**no-image-available**", 285 | NEWLINE, 286 | ].join("\n") 287 | ) 288 | ) 289 | .join("\n\n"), 290 | ]; 291 | 292 | const markdown = markdownLines.join("\n"); 293 | writeFileSync("result.md", markdown); 294 | writeFileSync("report.vega.json", JSON.stringify(vega, null, 2)); 295 | } 296 | 297 | const artifactsRootPath = process.argv[2] || __dirname; 298 | 299 | generateReport(artifactsRootPath).catch(console.error); 300 | -------------------------------------------------------------------------------- /federation/scenarios/ramping-vus/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fed-ramping-vus", 3 | "description": "This scenario runs 4 subgraphs and a GraphQL gateway with Federation spec, and runs a heavy query. We are running a heavy load of concurrent VUs to measure response time and other stats, during stress. It measure things like memory usage, CPU usage, response times. It also includes a summary of the entire execution, and metrics information about HTTP execution times.", 4 | "scripts": { 5 | "generate-report": "tsx generate-report.ts", 6 | "postinstall": "patch-package" 7 | }, 8 | "dependencies": { 9 | "vega": "6.1.2", 10 | "vega-lite": "6.1.0", 11 | "tablemark": "3.1.0", 12 | "tsx": "4.19.4", 13 | "typescript": "5.8.3", 14 | "graphql": "16.11.0", 15 | "patch-package": "8.0.0" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "22.15.24" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /federation/scenarios/ramping-vus/patches/vega-canvas+2.0.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/vega-canvas/build/vega-canvas.node.js b/node_modules/vega-canvas/build/vega-canvas.node.js 2 | index 0dad5f9..65ecadd 100644 3 | --- a/node_modules/vega-canvas/build/vega-canvas.node.js 4 | +++ b/node_modules/vega-canvas/build/vega-canvas.node.js 5 | @@ -13,7 +13,7 @@ const domImage = () => typeof Image !== 'undefined' ? Image : null; 6 | 7 | let NodeCanvas; 8 | try { 9 | - NodeCanvas = await import('canvas'); 10 | + NodeCanvas = require('canvas'); 11 | if (!(NodeCanvas && NodeCanvas.createCanvas)) { 12 | NodeCanvas = null; 13 | } 14 | -------------------------------------------------------------------------------- /federation/scenarios/ramping-vus/run.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | set -e 3 | 4 | if [ ! -n "$1" ]; then 5 | echo "gateway dir is missing, please run as: ./run.sh " 6 | exit 1 7 | fi 8 | 9 | export BASE_DIR=$( realpath ./ ) 10 | 11 | export LOCAL_ENV_OVERRIDE="" 12 | 13 | if [[ -z "${CI}" ]]; then 14 | export LOCAL_ENV_OVERRIDE="-f ../../subgraphs/docker-compose.subgraphs.local.yaml" 15 | fi 16 | 17 | export OUT_DIR="../../gateways/$1" 18 | export COMPOSE_FLAGS="-f ../../../docker-compose.metrics.yaml -f ../../subgraphs/docker-compose.subgraphs.yaml $LOCAL_ENV_OVERRIDE -f $OUT_DIR/docker-compose.yaml" 19 | 20 | on_error(){ 21 | cd $BASE_DIR 22 | docker inspect gateway 23 | docker inspect accounts 24 | docker compose $COMPOSE_FLAGS ps -a 25 | docker compose $COMPOSE_FLAGS logs 26 | } 27 | 28 | trap 'on_error' ERR 29 | 30 | docker compose $COMPOSE_FLAGS up -d --wait --force-recreate --build 31 | 32 | if [[ -z "${CI}" ]]; then 33 | trap "docker compose $COMPOSE_FLAGS down && exit 0" INT 34 | fi 35 | 36 | export K6_PROMETHEUS_RW_SERVER_URL=http://localhost:9090/api/v1/write 37 | export K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM=true 38 | export START_TIME="$(date +%s)" 39 | 40 | k6 --out=experimental-prometheus-rw --out json=$OUT_DIR/k6_metrics.json run -e SUMMARY_PATH="$OUT_DIR" benchmark.k6.js 41 | 42 | sleep 2 43 | 44 | export END_TIME="$(date +%s)" 45 | 46 | docker logs gateway > $OUT_DIR/gateway_log.txt 47 | 48 | rm -rf $OUT_DIR/overview.png || echo "" 49 | npx --quiet capture-website-cli "http://localhost:3000/d/01npcT44k/k6?orgId=1&from=${START_TIME}000&to=${END_TIME}000&kiosk" --output $OUT_DIR/overview.png --width 1200 --height 850 50 | 51 | rm -rf $OUT_DIR/http.png || echo "" 52 | npx --quiet capture-website-cli "http://localhost:3000/d-solo/01npcT44k/k6?orgId=1&from=${START_TIME}000&to=${END_TIME}000&panelId=41" --output $OUT_DIR/http.png --width 1200 --height 850 53 | 54 | rm -rf $OUT_DIR/containers.png || echo "" 55 | npx --quiet capture-website-cli "http://localhost:3000/d/pMEd7m0Mz/cadvisor-exporter?orgId=1&var-host=All&var-container=accounts&var-container=inventory&var-container=products&var-container=reviews&from=${START_TIME}000&to=${END_TIME}000&kiosk" --output $OUT_DIR/containers.png --width 1200 --height 850 56 | 57 | if [[ -z "${CI}" ]]; then 58 | echo "Done, you can find some stats in Grafana: http://localhost:3000/d/01npcT44k/k6?orgId=1&from=${START_TIME}000&to=${END_TIME}000" 59 | echo "You can close this and terminate all running services by using Ctrl+C" 60 | while true; do sleep 10; done 61 | fi 62 | -------------------------------------------------------------------------------- /federation/subgraphs/Dockerfile.rust: -------------------------------------------------------------------------------- 1 | FROM rust:1.87.0 as builder 2 | 3 | WORKDIR /usr/src/subgraph 4 | COPY Cargo.toml Cargo.lock ./ 5 | RUN echo 'fn main() { println!("Dummy!"); }' > ./subgraph.rs 6 | RUN cargo build --release 7 | 8 | RUN rm ./subgraph.rs 9 | COPY subgraph.rs ./ 10 | RUN touch subgraph.rs 11 | RUN cargo build --release 12 | 13 | FROM debian:12.11 14 | COPY --from=builder /usr/src/subgraph/target/release/subgraph /usr/local/bin/subgraph 15 | 16 | CMD subgraph 17 | -------------------------------------------------------------------------------- /federation/subgraphs/accounts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "accounts" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | async-graphql = "6.0.11" 10 | async-graphql-axum = "6.0.11" 11 | axum = "0.6.2" 12 | lazy_static = "1.4.0" 13 | rand = "0.9.0" 14 | tokio = { version = "1.36.0", features = ["rt-multi-thread", "full"] } 15 | 16 | [[bin]] 17 | name = "subgraph" 18 | path = "subgraph.rs" 19 | -------------------------------------------------------------------------------- /federation/subgraphs/accounts/subgraph.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{ 2 | http::GraphiQLSource, EmptyMutation, EmptySubscription, Object, SDLExportOptions, Schema, 3 | SimpleObject, ID, 4 | }; 5 | use async_graphql_axum::GraphQL; 6 | use axum::{ 7 | http::{Request, StatusCode}, 8 | middleware::{self, Next}, 9 | response::{self, IntoResponse, Response}, 10 | routing::get, 11 | Router, Server, 12 | }; 13 | use rand::Rng; 14 | use std::env::var; 15 | 16 | #[macro_use] 17 | extern crate lazy_static; 18 | 19 | async fn graphiql() -> impl IntoResponse { 20 | response::Html(GraphiQLSource::build().endpoint("/graphql").finish()) 21 | } 22 | 23 | lazy_static! { 24 | static ref USERS: Vec = vec![ 25 | User { 26 | id: ID("1".to_string()), 27 | name: Some("Uri Goldshtein".to_string()), 28 | username: Some("urigo".to_string()), 29 | birthday: Some(1234567890), 30 | }, 31 | User { 32 | id: ID("2".to_string()), 33 | name: Some("Dotan Simha".to_string()), 34 | username: Some("dotansimha".to_string()), 35 | birthday: Some(1234567890), 36 | }, 37 | User { 38 | id: ID("3".to_string()), 39 | name: Some("Kamil Kisiela".to_string()), 40 | username: Some("kamilkisiela".to_string()), 41 | birthday: Some(1234567890), 42 | }, 43 | User { 44 | id: ID("4".to_string()), 45 | name: Some("Arda Tanrikulu".to_string()), 46 | username: Some("ardatan".to_string()), 47 | birthday: Some(1234567890), 48 | }, 49 | User { 50 | id: ID("5".to_string()), 51 | name: Some("Gil Gardosh".to_string()), 52 | username: Some("gilgardosh".to_string()), 53 | birthday: Some(1234567890), 54 | }, 55 | User { 56 | id: ID("6".to_string()), 57 | name: Some("Laurin Quast".to_string()), 58 | username: Some("laurin".to_string()), 59 | birthday: Some(1234567890), 60 | } 61 | ]; 62 | } 63 | 64 | #[derive(SimpleObject, Clone)] 65 | struct User { 66 | id: ID, 67 | name: Option, 68 | username: Option, 69 | birthday: Option, 70 | } 71 | 72 | impl User { 73 | fn me() -> User { 74 | USERS[0].clone() 75 | } 76 | } 77 | 78 | struct Query; 79 | 80 | #[Object(extends = true)] 81 | impl Query { 82 | async fn me(&self) -> Option { 83 | Some(User::me()) 84 | } 85 | 86 | async fn user(&self, id: ID) -> Option { 87 | USERS.iter().find(|user| user.id == id).cloned() 88 | } 89 | 90 | async fn users(&self) -> Option>> { 91 | Some(USERS.iter().map(|user| Some(user.clone())).collect()) 92 | } 93 | 94 | #[graphql(entity)] 95 | async fn find_user_by_id(&self, id: ID) -> User { 96 | USERS.iter().find(|user| user.id == id).cloned().unwrap() 97 | } 98 | } 99 | 100 | async fn delay_middleware(req: Request, next: Next) -> Result { 101 | let delay_ms: Option = std::env::var("SUBGRAPH_DELAY_MS").ok().and_then(|s| s.parse().ok()).filter(|d| *d != 0);; 102 | 103 | if let Some(delay_ms) = delay_ms { 104 | tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await; 105 | } 106 | 107 | Ok(next.run(req).await) 108 | } 109 | 110 | #[tokio::main] 111 | async fn main() { 112 | let schema = Schema::build(Query, EmptyMutation, EmptySubscription) 113 | .enable_federation() 114 | .finish(); 115 | let sdl = schema.sdl_with_options(SDLExportOptions::new().federation().compose_directive()); 116 | println!("GraphQL Federation SDL:\n{}", sdl); 117 | let host = var("HOST").unwrap_or("0.0.0.0".to_owned()); 118 | let port = var("PORT").unwrap_or("4001".to_owned()); 119 | let path = "/graphql"; 120 | 121 | let app = Router::new() 122 | .route(path, get(graphiql).post_service(GraphQL::new(schema))) 123 | .route_layer(middleware::from_fn(delay_middleware)) 124 | .route("/sdl", get(|| async move { response::Html(sdl.clone()) })); 125 | 126 | println!("GraphiQL IDE: http://{}:{}{}", host, port, path); 127 | Server::bind(&format!("{}:{}", host, port).parse().unwrap()) 128 | .serve(app.into_make_service()) 129 | .await 130 | .unwrap(); 131 | } 132 | -------------------------------------------------------------------------------- /federation/subgraphs/docker-compose.subgraphs.local.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | accounts: 3 | build: 4 | context: ${BASE_DIR:-.}/../../subgraphs/accounts 5 | dockerfile: ../Dockerfile.rust 6 | inventory: 7 | build: 8 | context: ${BASE_DIR:-.}/../../subgraphs/inventory 9 | dockerfile: ../Dockerfile.rust 10 | products: 11 | build: 12 | context: ${BASE_DIR:-.}/../../subgraphs/products 13 | dockerfile: ../Dockerfile.rust 14 | reviews: 15 | build: 16 | context: ${BASE_DIR:-.}/../../subgraphs/reviews 17 | dockerfile: ../Dockerfile.rust 18 | 19 | networks: 20 | test: 21 | name: test -------------------------------------------------------------------------------- /federation/subgraphs/docker-compose.subgraphs.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | accounts: 3 | image: '${DOCKER_REGISTRY}subgraph-accounts${DOCKER_TAG}' 4 | container_name: accounts 5 | networks: 6 | - test 7 | ports: 8 | - "0.0.0.0:4001:4001" 9 | environment: 10 | PORT: 4001 11 | SUBGRAPH_DELAY_MS: ${SUBGRAPH_DELAY_MS} 12 | healthcheck: 13 | test: ["CMD", "/usr/lib/apt/apt-helper", "download-file", "http://0.0.0.0:4001/graphql", "temp"] 14 | interval: 3s 15 | timeout: 5s 16 | retries: 10 17 | inventory: 18 | image: '${DOCKER_REGISTRY}subgraph-inventory${DOCKER_TAG}' 19 | container_name: inventory 20 | networks: 21 | - test 22 | ports: 23 | - 4002:4002 24 | environment: 25 | PORT: 4002 26 | SUBGRAPH_DELAY_MS: ${SUBGRAPH_DELAY_MS} 27 | healthcheck: 28 | test: ["CMD", "/usr/lib/apt/apt-helper", "download-file", "http://0.0.0.0:4002/graphql", "temp"] 29 | interval: 3s 30 | timeout: 5s 31 | retries: 10 32 | products: 33 | image: '${DOCKER_REGISTRY}subgraph-products${DOCKER_TAG}' 34 | container_name: products 35 | networks: 36 | - test 37 | ports: 38 | - "0.0.0.0:4003:4003" 39 | environment: 40 | PORT: 4003 41 | SUBGRAPH_DELAY_MS: ${SUBGRAPH_DELAY_MS} 42 | healthcheck: 43 | test: ["CMD", "/usr/lib/apt/apt-helper", "download-file", "http://0.0.0.0:4003/graphql", "temp"] 44 | interval: 3s 45 | timeout: 5s 46 | retries: 10 47 | reviews: 48 | image: '${DOCKER_REGISTRY}subgraph-reviews${DOCKER_TAG}' 49 | container_name: reviews 50 | networks: 51 | - test 52 | ports: 53 | - 4004:4004 54 | environment: 55 | PORT: 4004 56 | SUBGRAPH_DELAY_MS: ${SUBGRAPH_DELAY_MS} 57 | healthcheck: 58 | test: ["CMD", "/usr/lib/apt/apt-helper", "download-file", "http://0.0.0.0:4004/graphql", "temp"] 59 | interval: 3s 60 | timeout: 5s 61 | retries: 10 62 | 63 | networks: 64 | test: 65 | name: test 66 | -------------------------------------------------------------------------------- /federation/subgraphs/docker.hcl: -------------------------------------------------------------------------------- 1 | variable "DOCKER_REGISTRY" { 2 | default = "" 3 | } 4 | 5 | variable "COMMIT_SHA" { 6 | default = "" 7 | } 8 | 9 | function "image_tag" { 10 | params = [name, tag] 11 | result = notequal("", tag) ? "${DOCKER_REGISTRY}${name}:${tag}" : name 12 | } 13 | 14 | target "accounts" { 15 | context = "./accounts/" 16 | dockerfile = "../Dockerfile.rust" 17 | tags = [ 18 | image_tag("subgraph-accounts", COMMIT_SHA), 19 | ] 20 | } 21 | 22 | target "reviews" { 23 | context = "./reviews/" 24 | dockerfile = "../Dockerfile.rust" 25 | tags = [ 26 | image_tag("subgraph-reviews", COMMIT_SHA), 27 | ] 28 | } 29 | 30 | target "products" { 31 | context = "./products/" 32 | dockerfile = "../Dockerfile.rust" 33 | tags = [ 34 | image_tag("subgraph-products", COMMIT_SHA), 35 | ] 36 | } 37 | 38 | target "inventory" { 39 | context = "./inventory/" 40 | dockerfile = "../Dockerfile.rust" 41 | tags = [ 42 | image_tag("subgraph-inventory", COMMIT_SHA), 43 | ] 44 | } 45 | 46 | group "subgraphs" { 47 | targets = [ 48 | "accounts", 49 | "reviews", 50 | "products", 51 | "inventory" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /federation/subgraphs/inventory/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "accounts" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | async-graphql = "6.0.11" 10 | async-graphql-axum = "6.0.11" 11 | axum = "0.6.2" 12 | lazy_static = "1.4.0" 13 | rand = "0.9.0" 14 | tokio = { version = "1.36.0", features = ["rt-multi-thread", "full"] } 15 | 16 | [[bin]] 17 | name = "subgraph" 18 | path = "subgraph.rs" 19 | -------------------------------------------------------------------------------- /federation/subgraphs/inventory/subgraph.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{ 2 | http::GraphiQLSource, ComplexObject, EmptyMutation, EmptySubscription, Object, 3 | SDLExportOptions, Schema, SimpleObject, ID, 4 | }; 5 | use async_graphql_axum::GraphQL; 6 | use axum::{ 7 | http::{Request, StatusCode}, 8 | middleware::{from_fn, Next}, 9 | response::{self, IntoResponse, Response}, 10 | routing::get, 11 | Router, Server, 12 | }; 13 | use lazy_static::lazy_static; 14 | use rand::Rng; 15 | use std::env::var; 16 | 17 | async fn graphiql() -> impl IntoResponse { 18 | response::Html(GraphiQLSource::build().endpoint("/graphql").finish()) 19 | } 20 | lazy_static! { 21 | static ref INVENTORY: Vec = vec![ 22 | Product { 23 | upc: "1".to_string(), 24 | in_stock: Some(true), 25 | price: None, 26 | weight: None, 27 | }, 28 | Product { 29 | upc: "2".to_string(), 30 | in_stock: Some(false), 31 | price: None, 32 | weight: None, 33 | }, 34 | Product { 35 | upc: "3".to_string(), 36 | in_stock: Some(false), 37 | price: None, 38 | weight: None, 39 | }, 40 | Product { 41 | upc: "4".to_string(), 42 | in_stock: Some(false), 43 | price: None, 44 | weight: None, 45 | }, 46 | Product { 47 | upc: "5".to_string(), 48 | in_stock: Some(true), 49 | price: None, 50 | weight: None, 51 | }, 52 | Product { 53 | upc: "6".to_string(), 54 | in_stock: Some(true), 55 | price: None, 56 | weight: None, 57 | }, 58 | Product { 59 | upc: "7".to_string(), 60 | in_stock: Some(true), 61 | price: None, 62 | weight: None, 63 | }, 64 | Product { 65 | upc: "8".to_string(), 66 | in_stock: Some(false), 67 | price: None, 68 | weight: None, 69 | }, 70 | Product { 71 | upc: "9".to_string(), 72 | in_stock: Some(true), 73 | price: None, 74 | weight: None, 75 | } 76 | ]; 77 | } 78 | 79 | #[derive(SimpleObject, Clone)] 80 | #[graphql(extends, complex)] 81 | struct Product { 82 | #[graphql(external)] 83 | upc: String, 84 | #[graphql(external)] 85 | weight: Option, 86 | #[graphql(external)] 87 | price: Option, 88 | in_stock: Option, 89 | } 90 | 91 | #[ComplexObject] 92 | impl Product { 93 | #[graphql(requires = "price weight")] 94 | pub async fn shipping_estimate(&self) -> Option { 95 | if let Some(price) = self.price { 96 | if price > 1000 { 97 | return Some(0); 98 | } 99 | 100 | if let Some(weight) = self.weight { 101 | return Some(weight / 2); 102 | } 103 | } 104 | 105 | None 106 | } 107 | } 108 | 109 | struct Query; 110 | 111 | #[Object(extends = true)] 112 | impl Query { 113 | #[graphql(entity)] 114 | async fn find_product_by_id(&self, upc: ID) -> Product { 115 | INVENTORY 116 | .iter() 117 | .find(|product| product.upc == upc.to_string()) 118 | .unwrap() 119 | .clone() 120 | } 121 | } 122 | 123 | async fn delay_middleware(req: Request, next: Next) -> Result { 124 | let delay_ms: Option = std::env::var("SUBGRAPH_DELAY_MS").ok().and_then(|s| s.parse().ok()).filter(|d| *d != 0);; 125 | 126 | if let Some(delay_ms) = delay_ms { 127 | tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await; 128 | } 129 | 130 | Ok(next.run(req).await) 131 | } 132 | 133 | #[tokio::main] 134 | async fn main() { 135 | let schema = Schema::build(Query, EmptyMutation, EmptySubscription) 136 | .enable_federation() 137 | .finish(); 138 | let sdl = schema.sdl_with_options(SDLExportOptions::new().federation().compose_directive()); 139 | println!("GraphQL Federation SDL:\n{}", sdl); 140 | let host = var("HOST").unwrap_or("0.0.0.0".to_owned()); 141 | let port = var("PORT").unwrap_or("4001".to_owned()); 142 | let path = "/graphql"; 143 | let app = Router::new() 144 | .route(path, get(graphiql).post_service(GraphQL::new(schema))) 145 | .route_layer(from_fn(delay_middleware)) 146 | .route("/sdl", get(|| async move { response::Html(sdl.clone()) })); 147 | 148 | println!("GraphiQL IDE: http://{}:{}{}", host, port, path); 149 | Server::bind(&format!("{}:{}", host, port).parse().unwrap()) 150 | .serve(app.into_make_service()) 151 | .await 152 | .unwrap(); 153 | } 154 | -------------------------------------------------------------------------------- /federation/subgraphs/products/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "accounts" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | async-graphql = "6.0.11" 10 | async-graphql-axum = "6.0.11" 11 | axum = "0.6.2" 12 | lazy_static = "1.4.0" 13 | rand = "0.9.0" 14 | tokio = { version = "1.36.0", features = ["rt-multi-thread", "full"] } 15 | 16 | [[bin]] 17 | name = "subgraph" 18 | path = "subgraph.rs" 19 | -------------------------------------------------------------------------------- /federation/subgraphs/products/subgraph.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{ 2 | http::GraphiQLSource, EmptyMutation, EmptySubscription, Object, SDLExportOptions, Schema, 3 | SimpleObject, ID, 4 | }; 5 | use async_graphql_axum::GraphQL; 6 | use axum::{ 7 | http::{Request, StatusCode}, 8 | middleware::{from_fn, Next}, 9 | response::{self, IntoResponse, Response}, 10 | routing::get, 11 | Router, Server, 12 | }; 13 | use lazy_static::lazy_static; 14 | use rand::Rng; 15 | use std::env::var; 16 | async fn graphiql() -> impl IntoResponse { 17 | response::Html(GraphiQLSource::build().endpoint("/graphql").finish()) 18 | } 19 | 20 | lazy_static! { 21 | static ref PRODUCTS: Vec = vec![ 22 | Product { 23 | upc: "1".to_string(), 24 | name: Some("Table".to_string()), 25 | price: Some(899), 26 | weight: Some(100), 27 | }, 28 | Product { 29 | upc: "2".to_string(), 30 | name: Some("Couch".to_string()), 31 | price: Some(1299), 32 | weight: Some(1000), 33 | }, 34 | Product { 35 | upc: "3".to_string(), 36 | name: Some("Glass".to_string()), 37 | price: Some(15), 38 | weight: Some(20), 39 | }, 40 | Product { 41 | upc: "4".to_string(), 42 | name: Some("Chair".to_string()), 43 | price: Some(499), 44 | weight: Some(100), 45 | }, 46 | Product { 47 | upc: "5".to_string(), 48 | name: Some("TV".to_string()), 49 | price: Some(1299), 50 | weight: Some(1000), 51 | }, 52 | Product { 53 | upc: "6".to_string(), 54 | name: Some("Lamp".to_string()), 55 | price: Some(6999), 56 | weight: Some(300), 57 | }, 58 | Product { 59 | upc: "7".to_string(), 60 | name: Some("Grill".to_string()), 61 | price: Some(3999), 62 | weight: Some(2000), 63 | }, 64 | Product { 65 | upc: "8".to_string(), 66 | name: Some("Fridge".to_string()), 67 | price: Some(100000), 68 | weight: Some(6000), 69 | }, 70 | Product { 71 | upc: "9".to_string(), 72 | name: Some("Sofa".to_string()), 73 | price: Some(9999), 74 | weight: Some(800), 75 | } 76 | ]; 77 | } 78 | #[derive(SimpleObject, Clone)] 79 | struct Product { 80 | upc: String, 81 | name: Option, 82 | price: Option, 83 | weight: Option, 84 | } 85 | 86 | struct Query; 87 | 88 | #[Object(extends = true)] 89 | impl Query { 90 | async fn top_products( 91 | &self, 92 | #[graphql(default = 5)] first: Option, 93 | ) -> Option>> { 94 | Some( 95 | PRODUCTS 96 | .iter() 97 | .take(first.unwrap_or(5) as usize) 98 | .map(|product| Some(product.clone())) 99 | .collect(), 100 | ) 101 | } 102 | 103 | #[graphql(entity)] 104 | async fn find_product_by_upc(&self, upc: ID) -> Product { 105 | PRODUCTS 106 | .iter() 107 | .find(|product| product.upc == upc.as_str()) 108 | .unwrap() 109 | .clone() 110 | } 111 | } 112 | 113 | async fn delay_middleware(req: Request, next: Next) -> Result { 114 | let delay_ms: Option = std::env::var("SUBGRAPH_DELAY_MS").ok().and_then(|s| s.parse().ok()).filter(|d| *d != 0);; 115 | 116 | if let Some(delay_ms) = delay_ms { 117 | tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await; 118 | } 119 | 120 | Ok(next.run(req).await) 121 | } 122 | 123 | #[tokio::main] 124 | async fn main() { 125 | let schema = Schema::build(Query, EmptyMutation, EmptySubscription) 126 | .enable_federation() 127 | .finish(); 128 | let sdl = schema.sdl_with_options(SDLExportOptions::new().federation().compose_directive()); 129 | println!("GraphQL Federation SDL:\n{}", sdl); 130 | let host = var("HOST").unwrap_or("0.0.0.0".to_owned()); 131 | let port = var("PORT").unwrap_or("4001".to_owned()); 132 | let path = "/graphql"; 133 | let app = Router::new() 134 | .route(path, get(graphiql).post_service(GraphQL::new(schema))) 135 | .route_layer(from_fn(delay_middleware)) 136 | .route("/sdl", get(|| async move { response::Html(sdl.clone()) })); 137 | 138 | println!("GraphiQL IDE: http://{}:{}{}", host, port, path); 139 | Server::bind(&format!("{}:{}", host, port).parse().unwrap()) 140 | .serve(app.into_make_service()) 141 | .await 142 | .unwrap(); 143 | } 144 | -------------------------------------------------------------------------------- /federation/subgraphs/reviews/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "accounts" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | async-graphql = "6.0.11" 10 | async-graphql-axum = "6.0.11" 11 | axum = "0.6.2" 12 | lazy_static = "1.4.0" 13 | rand = "0.9.0" 14 | tokio = { version = "1.36.0", features = ["rt-multi-thread", "full"] } 15 | 16 | [[bin]] 17 | name = "subgraph" 18 | path = "subgraph.rs" 19 | -------------------------------------------------------------------------------- /federation/subgraphs/reviews/subgraph.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{ 2 | http::GraphiQLSource, ComplexObject, EmptyMutation, EmptySubscription, Object, 3 | SDLExportOptions, Schema, SimpleObject, ID, 4 | }; 5 | use async_graphql_axum::GraphQL; 6 | use axum::{ 7 | http::{Request, StatusCode}, 8 | middleware::{from_fn, Next}, 9 | response::{self, IntoResponse, Response}, 10 | routing::get, 11 | Router, Server, 12 | }; 13 | use lazy_static::lazy_static; 14 | use rand::Rng; 15 | use std::env::var; 16 | 17 | async fn graphiql() -> impl IntoResponse { 18 | response::Html(GraphiQLSource::build().endpoint("/graphql").finish()) 19 | } 20 | 21 | lazy_static! { 22 | static ref REVIEWS: Vec = vec![ 23 | Review { 24 | id: ID("1".to_string()), 25 | body: Some("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.".to_string()), 26 | product: Some(Product { 27 | upc: "1".to_string() 28 | }) 29 | }, 30 | Review { 31 | id: ID("2".to_string()), 32 | body: Some("Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi".to_string()), 33 | product: Some(Product { 34 | upc: "1".to_string() 35 | }) 36 | }, 37 | Review { 38 | id: ID("3".to_string()), 39 | body: Some("sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.".to_string()), 40 | product: Some(Product { 41 | upc: "1".to_string() 42 | }) 43 | }, 44 | Review { 45 | id: ID("4".to_string()), 46 | body: Some("Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem".to_string()), 47 | product: Some(Product { 48 | upc: "1".to_string() 49 | }) 50 | }, 51 | Review { 52 | id: ID("5".to_string()), 53 | body: Some("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.".to_string()), 54 | product: Some(Product { 55 | upc: "2".to_string() 56 | }) 57 | }, 58 | Review { 59 | id: ID("6".to_string()), 60 | body: Some("Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi".to_string()), 61 | product: Some(Product { 62 | upc: "2".to_string() 63 | }) 64 | }, 65 | Review { 66 | id: ID("7".to_string()), 67 | body: Some("sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.".to_string()), 68 | product: Some(Product { 69 | upc: "2".to_string() 70 | }) 71 | }, 72 | Review { 73 | id: ID("8".to_string()), 74 | body: Some("Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem".to_string()), 75 | product: Some(Product { 76 | upc: "2".to_string() 77 | }) 78 | }, 79 | Review { 80 | id: ID("9".to_string()), 81 | body: Some("Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem".to_string()), 82 | product: Some(Product { 83 | upc: "3".to_string() 84 | }) 85 | }, 86 | Review { 87 | id: ID("10".to_string()), 88 | body: Some("Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem".to_string()), 89 | product: Some(Product { 90 | upc: "4".to_string() 91 | }) 92 | }, 93 | Review { 94 | id: ID("11".to_string()), 95 | body: Some("At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.".to_string()), 96 | product: Some(Product { 97 | upc: "4".to_string() 98 | }) 99 | } 100 | ]; 101 | } 102 | 103 | #[derive(SimpleObject, Clone)] 104 | #[graphql(complex)] 105 | struct Review { 106 | pub id: ID, 107 | pub body: Option, 108 | pub product: Option, 109 | } 110 | 111 | #[ComplexObject] 112 | impl Review { 113 | #[graphql(provides = "username")] 114 | pub async fn author(&self) -> Option { 115 | Some(User { 116 | id: "1".into(), 117 | username: Some("urigo".to_string()), 118 | reviews: Some(REVIEWS[0..2].iter().map(|r| Some(r.clone())).collect()), 119 | }) 120 | } 121 | } 122 | 123 | #[derive(SimpleObject)] 124 | #[graphql(extends)] 125 | struct User { 126 | #[graphql(external)] 127 | id: ID, 128 | #[graphql(external)] 129 | username: Option, 130 | reviews: Option>>, 131 | } 132 | 133 | #[derive(SimpleObject, Clone)] 134 | #[graphql(extends, complex)] 135 | 136 | struct Product { 137 | #[graphql(external)] 138 | upc: String, 139 | } 140 | 141 | #[ComplexObject] 142 | impl Product { 143 | pub async fn reviews(&self) -> Option>> { 144 | let relevant = REVIEWS 145 | .iter() 146 | .filter(|r| { 147 | if let Some(product) = &r.product { 148 | product.upc == self.upc 149 | } else { 150 | false 151 | } 152 | }) 153 | .map(|r| Some(r.clone())) 154 | .collect(); 155 | 156 | Some(relevant) 157 | } 158 | } 159 | 160 | struct Query; 161 | 162 | #[Object(extends = true)] 163 | impl Query { 164 | #[graphql(entity)] 165 | async fn find_review_by_id(&self, id: ID) -> Review { 166 | REVIEWS.iter().find(|r| r.id == id).unwrap().clone() 167 | } 168 | 169 | #[graphql(entity)] 170 | async fn find_user_by_id(&self, id: ID) -> User { 171 | User { 172 | id: id.into(), 173 | username: Some("user".to_string()), 174 | reviews: Some(REVIEWS[0..2].iter().map(|r| Some(r.clone())).collect()), 175 | } 176 | } 177 | 178 | #[graphql(entity)] 179 | async fn find_product_by_id(&self, upc: ID) -> Product { 180 | Product { 181 | upc: upc.to_string(), 182 | } 183 | } 184 | } 185 | 186 | async fn delay_middleware(req: Request, next: Next) -> Result { 187 | let delay_ms: Option = std::env::var("SUBGRAPH_DELAY_MS").ok().and_then(|s| s.parse().ok()).filter(|d| *d != 0);; 188 | 189 | if let Some(delay_ms) = delay_ms { 190 | tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await; 191 | } 192 | 193 | Ok(next.run(req).await) 194 | } 195 | 196 | #[tokio::main] 197 | async fn main() { 198 | let schema = Schema::build(Query, EmptyMutation, EmptySubscription) 199 | .enable_federation() 200 | .finish(); 201 | let sdl = schema.sdl_with_options(SDLExportOptions::new().federation().compose_directive()); 202 | println!("GraphQL Federation SDL:\n{}", sdl); 203 | let host = var("HOST").unwrap_or("0.0.0.0".to_owned()); 204 | let port = var("PORT").unwrap_or("4001".to_owned()); 205 | let path = "/graphql"; 206 | let app = Router::new() 207 | .route(path, get(graphiql).post_service(GraphQL::new(schema))) 208 | .route_layer(from_fn(delay_middleware)) 209 | .route("/sdl", get(|| async move { response::Html(sdl.clone()) })); 210 | 211 | println!("GraphiQL IDE: http://{}:{}{}", host, port, path); 212 | Server::bind(&format!("{}:{}", host, port).parse().unwrap()) 213 | .serve(app.into_make_service()) 214 | .await 215 | .unwrap(); 216 | } 217 | -------------------------------------------------------------------------------- /grafana/dashboards/dashboard.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | providers: 3 | - name: 'default' 4 | org_id: 1 5 | folder: '' 6 | type: 'file' 7 | options: 8 | path: /etc/grafana/provisioning/json-dashboards 9 | -------------------------------------------------------------------------------- /grafana/datasources/datasource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | access: proxy 7 | url: http://prometheus:9090 8 | jsonData: 9 | httpMethod: POST 10 | manageAlerts: true 11 | prometheusType: Prometheus 12 | prometheusVersion: 2.42.0 -------------------------------------------------------------------------------- /prometheus/prometheus.yaml: -------------------------------------------------------------------------------- 1 | scrape_configs: 2 | - job_name: cadvisor 3 | scrape_interval: 5s 4 | static_configs: 5 | - targets: 6 | - cadvisor:8080 -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | "github>the-guild-org/shared-config:renovate" 6 | ], 7 | "lockFileMaintenance": { 8 | "enabled": true 9 | }, 10 | "ignoreDeps": [ 11 | "axum", 12 | "async-graphql", 13 | "async-graphql-axum" 14 | ], 15 | "automerge": true 16 | } 17 | -------------------------------------------------------------------------------- /scripts/images-cleanup.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Important Note! 3 | // 4 | // Add a timeout to the script to prevent it from running indefinitely. 5 | // Add it in the github action file where the script is called. 6 | 7 | const { 8 | CF_IMAGES_LINK, 9 | CF_IMAGES_TOKEN, 10 | GITHUB_RUN_ID = "local", 11 | }: { 12 | CF_IMAGES_LINK: string; 13 | CF_IMAGES_TOKEN: string; 14 | GITHUB_RUN_ID?: string; 15 | } = (process as any).env; 16 | 17 | // CF_IMAGES_LINK = https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1 18 | // -> 19 | // https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v2 20 | const listEndpoint = 21 | CF_IMAGES_LINK.replace("/images/v1", "/images/v2") + "?sort_order=asc"; 22 | const deleteEndpoint = CF_IMAGES_LINK; 23 | 24 | const now = new Date(); 25 | // 1 month ago 26 | const thresholdDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); 27 | 28 | async function main() { 29 | const stack: Array = []; 30 | 31 | let continuation_token: string | null = null; 32 | 33 | do { 34 | const { result } = await listImages(continuation_token); 35 | 36 | if (!result) { 37 | throw new Error("Failed to list images from Cloudflare"); 38 | } 39 | 40 | for (const image of result.images) { 41 | const uploaded = new Date(image.uploaded); 42 | 43 | if (isOlderThanThreshold(uploaded)) { 44 | if (isBenchmarkImage(image.filename)) { 45 | stack.push(image.id); 46 | } 47 | } else { 48 | // The images are sorted by uploaded date in ascending order 49 | // so we can break the loop when we find an image that is newer 50 | break; 51 | } 52 | } 53 | 54 | continuation_token = result.continuation_token; 55 | } while (continuation_token); 56 | 57 | console.log(`Found ${stack.length} images to delete`); 58 | 59 | // Delete 5 images at a time 60 | while (stack.length > 0) { 61 | // Pop 5 images from the stack 62 | const images = stack.splice(0, 5); 63 | 64 | // I know we could pop the next image when one of the images is finished deleting, 65 | // but I don't want to spend too much time on this script. 66 | await Promise.all(images.map((imageId) => deleteImages(imageId))); 67 | 68 | console.log(`Deleted ${images.length} images. ${stack.length} remaining`); 69 | } 70 | 71 | console.log("Finished cleaning up images"); 72 | } 73 | 74 | /** 75 | * @param {string} continuation_token Continuation token for a next page. List images V2 returns continuation_token 76 | */ 77 | async function listImages( 78 | continuation_token: string | null 79 | ): Promise { 80 | const res = await fetch( 81 | listEndpoint + 82 | // add continuation token if it exists 83 | // because the endpoint contains `?sort_order=asc` already 84 | // we need to add `&` 85 | (continuation_token ? `&continuation_token=${continuation_token}` : ""), 86 | { 87 | method: "GET", 88 | headers: { 89 | Authorization: `Bearer ${CF_IMAGES_TOKEN}`, 90 | "Content-Type": "application/json", 91 | }, 92 | } 93 | ); 94 | 95 | if (!res.ok) { 96 | throw new Error( 97 | `Failed to list images from Cloudflare: ${res.status} ${res.statusText}` 98 | ); 99 | } 100 | 101 | return res.json(); 102 | } 103 | 104 | async function deleteImages(image_id: string) { 105 | const res = await fetch(deleteEndpoint + "/" + image_id, { 106 | method: "DELETE", 107 | headers: { 108 | Authorization: `Bearer ${CF_IMAGES_TOKEN}`, 109 | "Content-Type": "application/json", 110 | }, 111 | }); 112 | 113 | if (!res.ok) { 114 | // If we're rate limited, stop 115 | if (res.status === 429) { 116 | throw new Error( 117 | `Rate limited while deleting image from Cloudflare: ${res.status} ${res.statusText}` 118 | ); 119 | } 120 | 121 | // We don't want to throw an error here, 122 | // because we want to continue deleting other images. 123 | // The image will be deleted eventually, in the next run. 124 | console.error( 125 | `Failed to delete image from Cloudflare: ${res.status} ${res.statusText}` 126 | ); 127 | } 128 | } 129 | 130 | function isOlderThanThreshold(date: Date) { 131 | return date.getTime() < thresholdDate.getTime(); 132 | } 133 | 134 | function isBenchmarkImage(filename: string) { 135 | return ( 136 | filename.endsWith("-http.png") || 137 | filename.endsWith("-overview.png") || 138 | filename.endsWith("-report.svg") 139 | ); 140 | } 141 | 142 | interface ListImageResult { 143 | errors: unknown[]; 144 | messages: string[]; 145 | result: { 146 | continuation_token: string; 147 | images: Array<{ 148 | id: string; 149 | filename: string; 150 | /** 151 | * Datetime in ISO format 152 | * @example "2014-01-02T02:20:00.123Z" 153 | */ 154 | uploaded: string; 155 | }>; 156 | }; 157 | success: boolean; 158 | } 159 | 160 | main() 161 | .then(() => { 162 | console.log("Finished successfully"); 163 | process.exit(0); 164 | }) 165 | .catch((err) => { 166 | console.error(err); 167 | process.exit(1); 168 | }); 169 | --------------------------------------------------------------------------------