├── .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 |
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 |
67 |
68 |
69 | **Subgraphs Overview**
70 |
71 |
72 |
73 |
74 |
75 | **HTTP Overview**
76 |
77 |
78 |
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 |
122 |
123 |
124 | **Subgraphs Overview**
125 |
126 |
127 |
128 |
129 |
130 | **HTTP Overview**
131 |
132 |
133 |
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 |
177 |
178 |
179 | **Subgraphs Overview**
180 |
181 |
182 |
183 |
184 |
185 | **HTTP Overview**
186 |
187 |
188 |
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 |
232 |
233 |
234 | **Subgraphs Overview**
235 |
236 |
237 |
238 |
239 |
240 | **HTTP Overview**
241 |
242 |
243 |
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 |
289 |
290 |
291 | **Subgraphs Overview**
292 |
293 |
294 |
295 |
296 |
297 | **HTTP Overview**
298 |
299 |
300 |
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 |
346 |
347 |
348 | **Subgraphs Overview**
349 |
350 |
351 |
352 |
353 |
354 | **HTTP Overview**
355 |
356 |
357 |
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 |
401 |
402 |
403 | **Subgraphs Overview**
404 |
405 |
406 |
407 |
408 |
409 | **HTTP Overview**
410 |
411 |
412 |
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 | ? `
`
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 | ? `
`
274 | : "**no-image-available**",
275 | NEWLINE,
276 | "**Subgraphs Overview**",
277 | NEWLINE,
278 | info.containersImageUrl
279 | ? `
`
280 | : "**no-image-available**",
281 | NEWLINE,
282 | "**HTTP Overview**",
283 | NEWLINE,
284 | info.httpImageUrl
285 | ? `
`
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 |
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 |
67 |
68 |
69 | **Subgraphs Overview**
70 |
71 |
72 |
73 |
74 |
75 | **HTTP Overview**
76 |
77 |
78 |
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 |
122 |
123 |
124 | **Subgraphs Overview**
125 |
126 |
127 |
128 |
129 |
130 | **HTTP Overview**
131 |
132 |
133 |
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 |
180 |
181 |
182 | **Subgraphs Overview**
183 |
184 |
185 |
186 |
187 |
188 | **HTTP Overview**
189 |
190 |
191 |
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 |
235 |
236 |
237 | **Subgraphs Overview**
238 |
239 |
240 |
241 |
242 |
243 | **HTTP Overview**
244 |
245 |
246 |
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 |
292 |
293 |
294 | **Subgraphs Overview**
295 |
296 |
297 |
298 |
299 |
300 | **HTTP Overview**
301 |
302 |
303 |
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 |
349 |
350 |
351 | **Subgraphs Overview**
352 |
353 |
354 |
355 |
356 |
357 | **HTTP Overview**
358 |
359 |
360 |
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 |
404 |
405 |
406 | **Subgraphs Overview**
407 |
408 |
409 |
410 |
411 |
412 | **HTTP Overview**
413 |
414 |
415 |
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 |
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 |
67 |
68 |
69 | **Subgraphs Overview**
70 |
71 |
72 |
73 |
74 |
75 | **HTTP Overview**
76 |
77 |
78 |
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 |
122 |
123 |
124 | **Subgraphs Overview**
125 |
126 |
127 |
128 |
129 |
130 | **HTTP Overview**
131 |
132 |
133 |
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 |
177 |
178 |
179 | **Subgraphs Overview**
180 |
181 |
182 |
183 |
184 |
185 | **HTTP Overview**
186 |
187 |
188 |
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 |
232 |
233 |
234 | **Subgraphs Overview**
235 |
236 |
237 |
238 |
239 |
240 | **HTTP Overview**
241 |
242 |
243 |
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 |
289 |
290 |
291 | **Subgraphs Overview**
292 |
293 |
294 |
295 |
296 |
297 | **HTTP Overview**
298 |
299 |
300 |
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 |
346 |
347 |
348 | **Subgraphs Overview**
349 |
350 |
351 |
352 |
353 |
354 | **HTTP Overview**
355 |
356 |
357 |
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 |
401 |
402 |
403 | **Subgraphs Overview**
404 |
405 |
406 |
407 |
408 |
409 | **HTTP Overview**
410 |
411 |
412 |
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 |
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 |
67 |
68 |
69 | **Subgraphs Overview**
70 |
71 |
72 |
73 |
74 |
75 | **HTTP Overview**
76 |
77 |
78 |
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 |
122 |
123 |
124 | **Subgraphs Overview**
125 |
126 |
127 |
128 |
129 |
130 | **HTTP Overview**
131 |
132 |
133 |
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 |
180 |
181 |
182 | **Subgraphs Overview**
183 |
184 |
185 |
186 |
187 |
188 | **HTTP Overview**
189 |
190 |
191 |
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 |
235 |
236 |
237 | **Subgraphs Overview**
238 |
239 |
240 |
241 |
242 |
243 | **HTTP Overview**
244 |
245 |
246 |
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 |
290 |
291 |
292 | **Subgraphs Overview**
293 |
294 |
295 |
296 |
297 |
298 | **HTTP Overview**
299 |
300 |
301 |
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 |
345 |
346 |
347 | **Subgraphs Overview**
348 |
349 |
350 |
351 |
352 |
353 | **HTTP Overview**
354 |
355 |
356 |
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 |
402 |
403 |
404 | **Subgraphs Overview**
405 |
406 |
407 |
408 |
409 |
410 | **HTTP Overview**
411 |
412 |
413 |
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 | ? `
`
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 | ? `
`
272 | : "**no-image-available**",
273 | NEWLINE,
274 | "**Subgraphs Overview**",
275 | NEWLINE,
276 | info.containersImageUrl
277 | ? `
`
278 | : "**no-image-available**",
279 | NEWLINE,
280 | "**HTTP Overview**",
281 | NEWLINE,
282 | info.httpImageUrl
283 | ? `
`
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 |
--------------------------------------------------------------------------------