Skip to content

Commit 2240409

Browse files
committed
upgrade undici, dont include staleIfError in maxTTL, remove cache timeout
1 parent 38602fc commit 2240409

File tree

5 files changed

+45
-296
lines changed

5 files changed

+45
-296
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,8 @@ const datasource = new (class MoviesAPI extends HTTPDataSource {
109109
'X-Foo': 'bar',
110110
},
111111
requestCache: {
112-
maxCacheTimeout: 50 // In case of the cache does not respond for any reason (ms).
113112
maxTtl: 1000 * 60 * 10, // 10min, will respond for 10min with the cached result (updated every 10min)
114-
maxTtlIfError: 1000 * 60 * 30, // 30min, will respond in an error case with the cached response (for further 20min)
113+
maxTtlIfError: 1000 * 60 * 30, // 30min, will respond with the cached response in case of an error (for further 20min)
115114
},
116115
})
117116
}

benchmarks/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ Compare `apollo-datasource-http` (HTTP1 + Undici Pool) with apollo's `apollo-dat
1111
```
1212
❯ node benchmarks/http.js
1313
{
14-
'apollo-datasource-rest (http1)': { startTime: 114330370557900n, endTime: 114331160850400n },
15-
'apollo-datasource-http (http1)': { startTime: 114330327205800n, endTime: 114330690627800n }
14+
'apollo-datasource-rest (http1)': { startTime: 5974754539400n, endTime: 5975292928900n },
15+
'apollo-datasource-http (http1)': { startTime: 5974751416200n, endTime: 5974986816000n }
1616
}
1717
Results for 1000 subsequent requests:
18-
apollo-datasource-rest (http1) | total time: 790292500ns (790.293ms)
19-
apollo-datasource-http (http1) | total time: 363422000ns (363.422ms)
18+
apollo-datasource-rest (http1) | total time: 538389500ns (538.389ms)
19+
apollo-datasource-http (http1) | total time: 235399800ns (235.400ms)
2020
---
21-
apollo-datasource-http (http1) <> apollo-datasource-rest (http1) percent change: -54.014%
21+
apollo-datasource-http (http1) <> apollo-datasource-rest (http1) percent change: -56.277%
2222
```
2323

24-
**Result:** `apollo-datasource-http` is around `54%` faster than `apollo-datasource-rest`
24+
**Result:** `apollo-datasource-http` is around `56%` faster than `apollo-datasource-rest`

package.json

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,7 @@
5555
"apollo-server-errors": "^2.5.0",
5656
"apollo-server-types": "^0.9.0",
5757
"graphql": "^15.5.1",
58-
"p-timeout": "^4.1.0",
59-
"secure-json-parse": "^2.4.0",
60-
"undici": "^4.1.0"
58+
"undici": "^4.4.2"
6159
},
6260
"devDependencies": {
6361
"@tsconfig/node12": "^1.0.9",
@@ -66,12 +64,12 @@
6664
"apollo-datasource-rest": "^0.14.0",
6765
"ava": "^3.15.0",
6866
"h2url": "^0.2.0",
69-
"nock": "^13.1.0",
67+
"nock": "^13.1.1",
7068
"nyc": "^15.1.0",
7169
"prettier": "^2.3.2",
72-
"release-it": "^14.10.0",
73-
"ts-node": "^10.0.0",
74-
"typescript": "^4.3.4",
70+
"release-it": "^14.11.3",
71+
"ts-node": "^10.2.0",
72+
"typescript": "^4.3.5",
7573
"uid": "^2.0.0"
7674
}
7775
}

src/http-data-source.ts

Lines changed: 25 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import { DataSource, DataSourceConfig } from 'apollo-datasource'
22
import { Pool } from 'undici'
33
import { STATUS_CODES } from 'http'
44
import QuickLRU from '@alloc/quick-lru'
5-
import pTimeout from 'p-timeout'
6-
import sjson from 'secure-json-parse'
75

86
import { KeyValueCache } from 'apollo-server-caching'
9-
import Dispatcher, { ResponseData } from 'undici/types/dispatcher'
7+
import Dispatcher, { HttpMethod, ResponseData } from 'undici/types/dispatcher'
108
import { toApolloError } from 'apollo-server-errors'
119
import { EventEmitter } from 'stream'
1210
import { Logger } from 'apollo-server-types'
@@ -28,12 +26,9 @@ export class RequestError<T = unknown> extends Error {
2826

2927
export type CacheTTLOptions = {
3028
requestCache?: {
31-
// In case of the cache does not respond for any reason. This defines the max duration (ms) until the operation is aborted.
32-
maxCacheTimeout: number
3329
// The maximum time an item is cached in seconds.
3430
maxTtl: number
35-
// The maximum time an item fetched from the cache is case of an error in seconds.
36-
// This value must be greater than `maxTtl`.
31+
// The maximum time the cache should be used when the re-fetch from the origin fails.
3732
maxTtlIfError: number
3833
}
3934
}
@@ -52,7 +47,7 @@ export type Request<T = unknown> = {
5247
json?: boolean
5348
origin: string
5449
path: string
55-
method: string
50+
method: HttpMethod
5651
headers: Dictionary<string>
5752
} & CacheTTLOptions
5853

@@ -267,6 +262,7 @@ export abstract class HTTPDataSource<TContext = any> extends DataSource {
267262
cacheKey: string,
268263
): Promise<Response<TResult>> {
269264
try {
265+
// in case of JSON set appropriate content-type header
270266
if (request.body !== null && typeof request.body === 'object') {
271267
if (request.headers['content-type'] === undefined) {
272268
request.headers['content-type'] = 'application/json; charset=utf-8'
@@ -286,43 +282,42 @@ export abstract class HTTPDataSource<TContext = any> extends DataSource {
286282
}
287283

288284
const responseData = await this.pool.request(requestOptions)
289-
responseData.body.setEncoding('utf8')
290285

291-
let data = ''
292-
for await (const chunk of responseData.body) {
293-
data += chunk
294-
}
295-
296-
let json = null
297-
if (responseData.headers['content-type']?.includes('application/json')) {
298-
if (data.length && typeof data === 'string') {
299-
json = sjson.parse(data)
300-
}
286+
let body = await responseData.body.text()
287+
// can we parse it as JSON?
288+
if (
289+
responseData.headers['content-type']?.includes('application/json') &&
290+
body.length &&
291+
typeof body === 'string'
292+
) {
293+
body = JSON.parse(body)
301294
}
302295

303296
const response: Response<TResult> = {
304297
isFromCache: false,
305298
memoized: false,
306299
...responseData,
307-
body: json ?? data,
300+
// in case of the server does not properly respond with JSON we pass it as text.
301+
// this is necessary since POST, DELETE don't always have a JSON body.
302+
body: body as unknown as TResult,
308303
}
309304

310305
this.onResponse<TResult>(request, response)
311306

312307
// let's see if we can fill the shared cache
313308
if (request.requestCache && this.isResponseCacheable<TResult>(request, response)) {
314-
response.maxTtl = Math.max(request.requestCache.maxTtl, request.requestCache.maxTtlIfError)
309+
response.maxTtl = request.requestCache.maxTtl
315310
const cachedResponse = JSON.stringify(response)
316311

317-
// respond with the result immedialty without waiting for the cache
312+
// respond with the result immediately without waiting for the cache
318313
this.cache
319314
.set(cacheKey, cachedResponse, {
320-
ttl: request.requestCache?.maxTtl,
315+
ttl: request.requestCache.maxTtl,
321316
})
322317
.catch((err) => this.logger?.error(err))
323318
this.cache
324319
.set(`staleIfError:${cacheKey}`, cachedResponse, {
325-
ttl: request.requestCache?.maxTtlIfError,
320+
ttl: request.requestCache.maxTtl + request.requestCache.maxTtlIfError,
326321
})
327322
.catch((err) => this.logger?.error(err))
328323
}
@@ -331,15 +326,12 @@ export abstract class HTTPDataSource<TContext = any> extends DataSource {
331326
} catch (error) {
332327
this.onError?.(error, request)
333328

329+
// in case of an error we try to respond with a stale result from the stale-if-error cache
334330
if (request.requestCache) {
335-
// short circuit in case of the cache does not fail fast enough for any reason
336-
const cacheItem = await pTimeout(
337-
this.cache.get(`staleIfError:${cacheKey}`),
338-
request.requestCache.maxCacheTimeout,
339-
)
331+
const cacheItem = await this.cache.get(`staleIfError:${cacheKey}`)
340332

341333
if (cacheItem) {
342-
const response: Response<TResult> = sjson.parse(cacheItem)
334+
const response: Response<TResult> = JSON.parse(cacheItem)
343335
response.isFromCache = true
344336
return response
345337
}
@@ -356,7 +348,7 @@ export abstract class HTTPDataSource<TContext = any> extends DataSource {
356348

357349
const cacheKey = this.onCacheKeyCalculation(request)
358350

359-
// check if we have any GET call in the cache to respond immediatly
351+
// check if we have any GET call in the cache to respond immediately
360352
if (request.method === 'GET') {
361353
// Memoize GET calls for the same data source instance
362354
// a single instance of the data sources is scoped to one graphql request
@@ -377,13 +369,9 @@ export abstract class HTTPDataSource<TContext = any> extends DataSource {
377369
// try to fetch from shared cache
378370
if (request.requestCache) {
379371
try {
380-
// short circuit in case of the cache does not fail fast enough for any reason
381-
const cacheItem = await pTimeout(
382-
this.cache.get(cacheKey),
383-
request.requestCache.maxCacheTimeout,
384-
)
372+
const cacheItem = await this.cache.get(cacheKey)
385373
if (cacheItem) {
386-
const cachedResponse: Response<TResult> = sjson.parse(cacheItem)
374+
const cachedResponse: Response<TResult> = JSON.parse(cacheItem)
387375
cachedResponse.memoized = false
388376
cachedResponse.isFromCache = true
389377
return cachedResponse

0 commit comments

Comments
 (0)