-
Notifications
You must be signed in to change notification settings - Fork 67
feat: Improve handling of HTTP/HTTPS agent sessions #6498
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
base: main
Are you sure you want to change the base?
Changes from 5 commits
68fd5d8
fdbad2c
ea8a2a2
6fb4d87
8ca6638
002f644
b784961
7af67b9
341b309
2c33adf
911cbb7
1e9e397
ff25ce0
b68bf7f
dc455b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| '@sap-cloud-sdk/connectivity': minor | ||
| '@sap-cloud-sdk/http-client': minor | ||
| --- | ||
|
|
||
| [Improvement] Cache custom http and https agents and enable the keep-alive option by default. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| '@sap-cloud-sdk/connectivity': minor | ||
| '@sap-cloud-sdk/http-client': minor | ||
| --- | ||
|
|
||
| [Improvement] Use node's global http/https agent unless a custom agent is required by the destination configuration. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,8 +1,14 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createHash } from 'node:crypto'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface CacheInterface<T> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hasKey(key: string): boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| get(key: string | undefined): T | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set(key: string | undefined, item: CacheEntry<T>): void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clear(): void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getOrInsertComputed( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key: string | undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| computeFn: () => CacheEntry<T> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): T; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -39,21 +45,25 @@ export class Cache<T> implements CacheInterface<T> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Object that stores all cached entries. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private cache: Record<string, CacheEntry<T>>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private cache: Map<string, CacheEntry<T>>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice 👍 Map maintains the insertion order. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Creates an instance of Cache. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param defaultValidityTime - The default validity time in milliseconds. Use 0 for unlimited cache duration. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param maxSize - The maximum number of entries in the cache. Use Infinity for unlimited size. Items are evicted based on a least recently used (LRU) strategy. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor(private defaultValidityTime: number) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cache = {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private defaultValidityTime: number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private maxSize = Infinity | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
davidkna-sap marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cache = new Map<string, CacheEntry<T>>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Clear all cached items. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clear(): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cache = {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cache.clear(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -62,7 +72,7 @@ export class Cache<T> implements CacheInterface<T> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns A boolean value that indicates whether the entry exists in cache. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hasKey(key: string): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.cache.hasOwnProperty(key); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.cache.has(key); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -71,9 +81,19 @@ export class Cache<T> implements CacheInterface<T> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns The corresponding entry to the provided key if it is still valid, returns `undefined` otherwise. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| get(key: string | undefined): T | undefined { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return key && this.hasKey(key) && !isExpired(this.cache[key]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? this.cache[key].entry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const entry = key ? this.cache.get(key) : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (entry) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isExpired(entry)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cache.delete(key!); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // LRU cache: Move accessed entry to the end of the Map to mark it as recently used | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.maxSize !== Infinity) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cache.delete(key!); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cache.set(key!, entry); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return entry?.entry; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [pp]
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handled this differently because |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -82,10 +102,41 @@ export class Cache<T> implements CacheInterface<T> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param item - The entry to cache. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set(key: string | undefined, item: CacheEntry<T>): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [q] In general, I wonder why this key can be undefined :)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Claude is claiming it's for the benefit of |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (key) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const expires = item.expires ?? this.inferDefaultExpirationTime(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cache[key] = { entry: item.entry, expires }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!key) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cache.size >= this.maxSize && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cache.size > 0 && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
davidkna-sap marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !this.cache.has(key) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Evict the least recently used (LRU) entry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lruKey = this.cache.keys().next().value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cache.delete(lruKey!); // SAFETY: size > 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [q] What does the comment tell me?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment explains why the |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.maxSize !== Infinity && this.cache.has(key)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If the key already exists, delete it to update its position in the LRU order | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cache.delete(key); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const expires = item.expires ?? this.inferDefaultExpirationTime(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cache.set(key, { entry: item.entry, expires }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getOrInsertComputed( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key: string | undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| computeFn: () => CacheEntry<T> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): T { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!key) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return computeFn().entry; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [req] I wasn't sure about this, because it seems a little off: It is weird, that this just calls the compute function without caching it. But, when you have a case where the key might not be defined and you want to have a default you don't have to differentiate the cases. So I guess it is better as is. But, I am missing signs that this is intended, though, aka. a test and docs.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've removed this special handling, I added in on purpose but is currently not a useful feature in practice. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cachedEntry = this.get(key); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (cachedEntry !== undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cachedEntry; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const newEntry = computeFn(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.set(key, newEntry); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return newEntry.entry; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private inferDefaultExpirationTime(): number | undefined { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -98,6 +149,17 @@ export class Cache<T> implements CacheInterface<T> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Hashes the given value to create a cache key. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @internal | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param value - The value to hash. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns A hash of the given value using a cryptographic hash function. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function hashCacheKey(value: Record<string, unknown>): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const serialized = JSON.stringify(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you checked if this is sufficient? JSON.stringify cannot stringify many things like Map or Set etc. Maybe check alternatives https://www.npmjs.com/package/fast-json-stable-stringify or something else.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I switched to I can switch to
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also did some search, but tbh none of them seems to be very actively maintained. Maybe they just work without problem. Also maybe we can try |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return createHash('blake2s256').update(serialized).digest('hex'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isExpired<T>(item: CacheEntry<T>): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (item.expires === undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.