Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/negative_cache_single_flight.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
hive-console-sdk: minor
hive-router: patch
---

# Negative Cache and Single-Flight

Introduced single-flight resolution of documents in the SDK.

Added a negative cache to store non 2XX requests for 5s (configurable, but in SDK it's disabled by default). It's meant to not keep repeating the same requests that eventually give errors or 404s.
35 changes: 35 additions & 0 deletions .changeset/persisted_documents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
hive-router-plan-executor: minor
hive-router-config: minor
hive-router: minor
hive-router-internal: minor
hive-console-sdk: minor
---

# Persisted Documents

Introduces persisted documents support in Hive Router with configurable extraction and storage backends.

Supports extracting persisted document IDs from:
- `documentId` in request body (default)
- `documentId` in URL query params (default)
- Apollo-style `extensions.persistedQuery.sha256Hash` (default)
- custom `json_path` (for example `doc_id` or `extensions.anything.id`)
- custom `url_query_param` (for example `?doc_id=123`)
- custom `url_path_param` (for example `/graphql/:id`)

Order is configurable and evaluated top-to-bottom.

Supports persisted document resolution from:
- file manifests (Apollo and Relay KV styles)
- Hive CDN (via `hive-console-sdk`)

File storage includes watch mode by default (with 150ms debounce) to reload manifests after file changes.
Hive storage validates document ID syntax before generating CDN paths to avoid silent invalid-path behavior.

Adds persisted-documents metrics:

- `hive.router.persisted_documents.extract.missing_id_total`
- `hive.router.persisted_documents.storage.failures_total`

These help track migration progress and resolution failures in production
13 changes: 12 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@ jobs:
binary: "hive_router_with_plugin"
config: "bench/configs/plugins.config.yaml"
compare_with_default: true
- name: "persisted-documents"
args: ""
package: "hive-router"
binary: "hive_router"
config: "bench/configs/persisted-documents.config.yaml"
compare_with_default: true
name: benchmark / router / ${{ matrix.name }}
runs-on: ubuntu-latest
env:
Expand Down Expand Up @@ -220,7 +226,12 @@ jobs:
ROUTER_CONFIG_FILE_PATH: ${{matrix.config}}
- name: Run k6 benchmark for ${{ github.ref }}
if: github.event_name == 'pull_request'
run: k6 run -e SUMMARY_PATH=./bench/results/pr bench/k6.js
run: |
if [ "${{ matrix.name }}" = "persisted-documents" ]; then
k6 run -e SUMMARY_PATH=./bench/results/pr -e BENCH_PERSISTED_MODE=true -e BENCH_DOCUMENT_ID=bench_test_query bench/k6.js
else
k6 run -e SUMMARY_PATH=./bench/results/pr bench/k6.js
fi
- name: Checkout main branch in a separate directory
if: github.event_name == 'pull_request'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
Expand Down
90 changes: 89 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ strum = { version = "0.28.0", features = ["derive"] }
mockito = "1.7.0"
futures-util = "0.3.31"
axum = "0.8.4"
notify = "8.2.0"

# Telemetry
opentelemetry = "0.31.0"
Expand Down
15 changes: 15 additions & 0 deletions bench/configs/persisted-documents.config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=../../router-config.schema.json
supergraph:
source: file
path: ../supergraph.graphql

persisted_documents:
enabled: true
require_id: true
storage:
type: file
path: ../persisted-documents.json
watch: false

log:
level: info
14 changes: 9 additions & 5 deletions bench/k6.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { textSummary } from "https://jslib.k6.io/k6-summary/0.0.1/index.js";
const endpoint = __ENV.ROUTER_ENDPOINT || "http://0.0.0.0:4000/graphql";
const vus = __ENV.BENCH_VUS ? parseInt(__ENV.BENCH_VUS) : 50;
const duration = __ENV.BENCH_OVER_TIME || "30s";
const persistedMode = __ENV.BENCH_PERSISTED_MODE === "true";
const documentId = __ENV.BENCH_DOCUMENT_ID || "bench_test_query";

export const options = {
vus,
Expand Down Expand Up @@ -42,9 +44,7 @@ function runOnce(identifier, cb) {
return cb();
}

const graphqlRequest = {
payload: JSON.stringify({
query: `fragment User on User {
const graphqlQuery = `fragment User on User {
id
username
name
Expand Down Expand Up @@ -101,8 +101,12 @@ const graphqlRequest = {
}
}
}
}`,
}),
}`;

const graphqlRequest = {
payload: JSON.stringify(
persistedMode ? { documentId } : { query: graphqlQuery },
),
params: {
headers: {
"Content-Type": "application/json",
Expand Down
3 changes: 3 additions & 0 deletions bench/persisted-documents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"bench_test_query": "fragment User on User { id username name } fragment Review on Review { id body } fragment Product on Product { inStock name price shippingEstimate upc weight } query TestQuery { users { ...User reviews { ...Review product { ...Product reviews { ...Review author { ...User reviews { ...Review product { ...Product } } } } } } } topProducts { ...Product reviews { ...Review author { ...User reviews { ...Review product { ...Product } } } } } }"
}
6 changes: 5 additions & 1 deletion bin/router/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ ahash = { workspace = true }
rustls = { workspace = true, features = ["aws-lc-rs"] }
hyper-rustls = { workspace = true, features = ["aws-lc-rs"]}
dashmap = { workspace = true }
notify = { workspace = true }
memchr = "2.8.0"
percent-encoding = "2.3.2"
matchit = "0.9.1"

moka = { workspace = true }
ulid = "1.2.1"
Expand Down Expand Up @@ -85,5 +89,5 @@ criterion = { workspace = true }
insta = { workspace = true }

[[bench]]
name = "router_benches"
name = "persisted_documents_matcher_benches"
harness = false
Loading
Loading