Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cacheable Queries #2068

Open
malaquf opened this issue Jan 13, 2025 · 0 comments
Open

Cacheable Queries #2068

malaquf opened this issue Jan 13, 2025 · 0 comments
Labels
type: enhancement New feature or request

Comments

@malaquf
Copy link
Contributor

malaquf commented Jan 13, 2025

Is your feature request related to a problem? Please describe.
I believe a common requirement for GraphQL is proper edge caching with per query cache duration, and having this functionality embedded in the framework would make user's life much easier.

Describe the solution you'd like
We tackled this problem by defining a CacheableQuery, which extends Query, and contain a edgeCacheDuration field as follows.

interface CacheableQuery : Query {
	val edgeCacheDuration: CacheDuration
}
enum class CacheDuration(val duration: Duration) {
	FIFTEEN_SECONDS(Duration.ofSeconds(15)),
	THIRTY_SECONDS(Duration.ofSeconds(30)),
	ONE_MINUTE(Duration.ofMinutes(1)),
	TWO_MINUTES(Duration.ofMinutes(2)),
	FIVE_MINUTES(Duration.ofMinutes(5)),
	TEN_MINUTES(Duration.ofMinutes(10)),
	FIFTEEN_MINUTES(Duration.ofMinutes(15)),
	ONE_HOUR(Duration.ofHours(1)),
	HALF_A_DAY(Duration.ofHours(12)),
}

Then whenever defining a query class, we have something like:

@Component
class MyCacheableQuery : CacheableQuery {

	override val edgeCacheDuration: CacheDuration = CacheDuration.FIVE_MINUTES
...
}

Then we define a CacheControlFilter, which scans a request body (or retrieves the preparsed document if it's an APQ GET with hash only) and finds the minimum edgeCacheDuration and sets it in the control header. Something like the following:

override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
		val response = exchange.response
		val request = exchange.request
		val decoratedResponse = object : ServerHttpResponseDecorator(response) {
			override fun writeWith(body: Publisher<out DataBuffer>): Mono<Void> {
				if (request.isGetPersistedQuery()) {
					request.getAutomaticPersistedQueryExtension()?.let { extensions ->
						val entry = apqCache.get(extensions.sha256Hash)
						if (entry != null) {
							entry.document.definitions
								.filterIsInstance<OperationDefinition>()
								.filter { it.operation == OperationDefinition.Operation.QUERY }
								.flatMap {
									it.selectionSet.getSelectionsOfType(Field::class.java).map { field -> field.name }
								}
								.minOfOrNull { edgeCacheDuration.getValue(it) }?.let { duration ->
									headers.cacheControl = "max-age=$duration"
								}
						}
					}
				}
				return super.writeWith(body)
			}
		}
		val decoratedRequest = object : ServerHttpRequestDecorator(request) {
			override fun getBody(): Flux<DataBuffer> =
				super.getBody().doOnNext { dataBuffer ->
					val body = dataBuffer.toString(StandardCharsets.UTF_8)
					val duration = extractQueries(body).minOfOrNull { edgeCacheDuration.getValue(it) } ?: 0
					if (duration > 0) decoratedResponse.headers.cacheControl = "max-age=$duration"
				}
		}
		return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build())
	}

Describe alternatives you've considered
This feature can probably be achieved by using apollo server, for example, but in many cases, apollo server usage is not really needed/desired, and a simpler solution can be done directly in the graphql framework within the application.

Additional context
n/a

@malaquf malaquf added the type: enhancement New feature or request label Jan 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement New feature or request
Development

No branches or pull requests

1 participant