mirror of
https://github.com/graphql-hive/console
synced 2026-05-24 09:38:26 +00:00
fix(apollo): fix l2 cache config and add waitUntil support (#7462)
This commit is contained in:
parent
d00dd221ec
commit
60133a41a6
4 changed files with 101 additions and 11 deletions
55
.changeset/warm-melons-argue.md
Normal file
55
.changeset/warm-melons-argue.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
'@graphql-hive/core': minor
|
||||
'@graphql-hive/yoga': minor
|
||||
'@graphql-hive/apollo': minor
|
||||
---
|
||||
|
||||
Add Layer 2 (L2) cache support for persisted documents.
|
||||
|
||||
This feature adds a second layer of caching between the in-memory cache (L1) and the CDN for persisted documents. This is particularly useful for:
|
||||
|
||||
- **Serverless environments**: Where in-memory cache is lost between invocations
|
||||
- **Multi-instance deployments**: To share cached documents across server instances
|
||||
- **Reducing CDN calls**: By caching documents in Redis or similar external caches
|
||||
|
||||
The lookup flow is: L1 (memory) -> L2 (Redis/external) -> CDN
|
||||
|
||||
**Example with GraphQL Yoga:**
|
||||
|
||||
```typescript
|
||||
import { createYoga } from 'graphql-yoga'
|
||||
import { useHive } from '@graphql-hive/yoga'
|
||||
import { createClient } from 'redis'
|
||||
|
||||
const redis = createClient({ url: 'redis://localhost:6379' })
|
||||
await redis.connect()
|
||||
|
||||
const yoga = createYoga({
|
||||
plugins: [
|
||||
useHive({
|
||||
experimental__persistedDocuments: {
|
||||
cdn: {
|
||||
endpoint: 'https://cdn.graphql-hive.com/artifacts/v1/<target_id>',
|
||||
accessToken: '<cdn_access_token>'
|
||||
},
|
||||
layer2Cache: {
|
||||
cache: {
|
||||
get: (key) => redis.get(`hive:pd:${key}`),
|
||||
set: (key, value, opts) =>
|
||||
redis.set(`hive:pd:${key}`, value, opts?.ttl ? { EX: opts.ttl } : {})
|
||||
},
|
||||
ttlSeconds: 3600, // 1 hour for found documents
|
||||
notFoundTtlSeconds: 60 // 1 minute for not-found (negative caching)
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Configurable TTL for found documents (`ttlSeconds`)
|
||||
- Configurable TTL for negative caching (`notFoundTtlSeconds`)
|
||||
- Graceful fallback to CDN if L2 cache fails
|
||||
- Support for `waitUntil` in serverless environments
|
||||
- Apollo Server integration auto-uses context cache if available
|
||||
|
|
@ -193,13 +193,16 @@ export function createHive(clientOrOptions: HivePluginOptions, ctx?: GraphQLServ
|
|||
experimental__persistedDocuments: clientOrOptions.experimental__persistedDocuments
|
||||
? {
|
||||
...clientOrOptions.experimental__persistedDocuments,
|
||||
layer2Cache:
|
||||
persistedDocumentsCache || clientOrOptions.experimental__persistedDocuments.layer2Cache
|
||||
? {
|
||||
cache: persistedDocumentsCache!,
|
||||
...(clientOrOptions.experimental__persistedDocuments.layer2Cache || {}),
|
||||
}
|
||||
: undefined,
|
||||
layer2Cache: (() => {
|
||||
const userL2Config = clientOrOptions.experimental__persistedDocuments?.layer2Cache;
|
||||
if (persistedDocumentsCache) {
|
||||
return {
|
||||
cache: persistedDocumentsCache,
|
||||
...(userL2Config || {}),
|
||||
};
|
||||
}
|
||||
return userL2Config;
|
||||
})(),
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
|
@ -312,8 +315,13 @@ export function useHive(clientOrOptions: HiveClient | HivePluginOptions): Apollo
|
|||
) {
|
||||
persistedDocumentHash = context.request.http.body.documentId;
|
||||
try {
|
||||
// Pass waitUntil from context if available for serverless environments
|
||||
const contextValue = isLegacyV3
|
||||
? (context as any).context
|
||||
: (context as any).contextValue;
|
||||
const document = await hive.experimental__persistedDocuments.resolve(
|
||||
context.request.http.body.documentId,
|
||||
{ waitUntil: contextValue?.waitUntil },
|
||||
);
|
||||
|
||||
if (document) {
|
||||
|
|
|
|||
|
|
@ -112,10 +112,27 @@ export function createPersistedDocuments(
|
|||
|
||||
// L2
|
||||
const layer2Cache: PersistedDocumentsCache | undefined = config.layer2Cache?.cache;
|
||||
const layer2TtlSeconds = config.layer2Cache?.ttlSeconds;
|
||||
const layer2NotFoundTtlSeconds = config.layer2Cache?.notFoundTtlSeconds ?? 60;
|
||||
let layer2TtlSeconds = config.layer2Cache?.ttlSeconds;
|
||||
let layer2NotFoundTtlSeconds: number | undefined = config.layer2Cache?.notFoundTtlSeconds ?? 60;
|
||||
const layer2KeyPrefix = config.layer2Cache?.keyPrefix ?? '';
|
||||
const layer2WaitUntil = config.layer2Cache?.waitUntil;
|
||||
|
||||
// Validate L2 cache options
|
||||
if (layer2TtlSeconds !== undefined && layer2TtlSeconds < 0) {
|
||||
config.logger.warn(
|
||||
'Negative ttlSeconds (%d) provided for L2 cache; treating as no expiration',
|
||||
layer2TtlSeconds,
|
||||
);
|
||||
layer2TtlSeconds = undefined;
|
||||
}
|
||||
if (layer2NotFoundTtlSeconds !== undefined && layer2NotFoundTtlSeconds < 0) {
|
||||
config.logger.warn(
|
||||
'Negative notFoundTtlSeconds (%d) provided for L2 cache; treating as no expiration',
|
||||
layer2NotFoundTtlSeconds,
|
||||
);
|
||||
layer2NotFoundTtlSeconds = undefined;
|
||||
}
|
||||
|
||||
let allowArbitraryDocuments: (context: { headers?: HeadersObject }) => PromiseOrValue<boolean>;
|
||||
|
||||
if (typeof config.allowArbitraryDocuments === 'boolean') {
|
||||
|
|
@ -178,7 +195,7 @@ export function createPersistedDocuments(
|
|||
|
||||
let cached: string | typeof PERSISTED_DOCUMENT_NOT_FOUND | null;
|
||||
try {
|
||||
cached = await layer2Cache.get(documentId);
|
||||
cached = await layer2Cache.get(layer2KeyPrefix + documentId);
|
||||
} catch (error) {
|
||||
// L2 cache failure should not break the request
|
||||
config.logger.warn('L2 cache get failed for document %s: %O', documentId, error);
|
||||
|
|
@ -220,7 +237,11 @@ export function createPersistedDocuments(
|
|||
const ttl = value === null ? layer2NotFoundTtlSeconds : layer2TtlSeconds;
|
||||
|
||||
// Fire-and-forget. don't await, don't block
|
||||
const setPromise = layer2Cache.set(documentId, cacheValue, ttl ? { ttl } : undefined);
|
||||
const setPromise = layer2Cache.set(
|
||||
layer2KeyPrefix + documentId,
|
||||
cacheValue,
|
||||
ttl ? { ttl } : undefined,
|
||||
);
|
||||
if (setPromise) {
|
||||
const handledPromise: Promise<void> = Promise.resolve(setPromise).then(
|
||||
() => {
|
||||
|
|
|
|||
|
|
@ -378,6 +378,12 @@ export type Layer2CacheConfiguration = {
|
|||
*/
|
||||
notFoundTtlSeconds?: number;
|
||||
|
||||
/**
|
||||
* Key prefix for cached persisted documents.
|
||||
* @default "" (no prefix)
|
||||
*/
|
||||
keyPrefix?: string;
|
||||
|
||||
/**
|
||||
* Optional function to register background work in serverless environments if not available in context.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue