From 9bc8b05af38a55a16f0a8a27bdb0c29ed3d7e6c0 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Fri, 2 Sep 2022 13:32:34 +0300
Subject: [PATCH] feat: http keepalives (#17582)

---
 docs/usage/configuration-options.md |  4 ++++
 lib/config/options/index.ts         | 11 +++++++++++
 lib/types/host-rules.ts             |  1 +
 lib/util/http/host-rules.spec.ts    |  7 +++++++
 lib/util/http/host-rules.ts         |  5 +++++
 lib/util/http/keepalive.ts          | 12 ++++++++++++
 package.json                        |  1 +
 yarn.lock                           |  2 +-
 8 files changed, 42 insertions(+), 1 deletion(-)
 create mode 100644 lib/util/http/keepalive.ts

diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index d61dbf2b75..07f3fdfe92 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -1183,6 +1183,10 @@ Example:
 }
 ```
 
+### keepalive
+
+If enabled, this allows a single TCP connection to remain open for multiple HTTP(S) requests/responses.
+
 ### matchHost
 
 This can be a base URL (e.g. `https://api.github.com`) or a hostname like `github.com` or `api.github.com`.
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index c0c3df0b49..4c007eedeb 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -2115,6 +2115,17 @@ const options: RenovateOptions[] = [
     env: false,
     experimental: true,
   },
+  {
+    name: 'keepalive',
+    description: 'Enable http keepalives for hosts',
+    type: 'boolean',
+    stage: 'repository',
+    parent: 'hostRules',
+    default: false,
+    cli: false,
+    env: false,
+    experimental: true,
+  },
   {
     name: 'prBodyDefinitions',
     description: 'Table column definitions for use in PR tables.',
diff --git a/lib/types/host-rules.ts b/lib/types/host-rules.ts
index e144850e46..5cc234b37f 100644
--- a/lib/types/host-rules.ts
+++ b/lib/types/host-rules.ts
@@ -12,6 +12,7 @@ export interface HostRuleSearchResult {
   concurrentRequestLimit?: number;
 
   dnsCache?: boolean;
+  keepalive?: boolean;
 }
 
 export interface HostRule extends HostRuleSearchResult {
diff --git a/lib/util/http/host-rules.spec.ts b/lib/util/http/host-rules.spec.ts
index 8be653b0d4..dc910cfac8 100644
--- a/lib/util/http/host-rules.spec.ts
+++ b/lib/util/http/host-rules.spec.ts
@@ -119,6 +119,13 @@ describe('util/http/host-rules', () => {
     });
   });
 
+  it('uses http keepalives', () => {
+    hostRules.add({ keepalive: true });
+    expect(
+      applyHostRules(url, { ...options, token: 'xxx' }).agent
+    ).toBeDefined();
+  });
+
   it('disables http2', () => {
     process.env.HTTP_PROXY = 'http://proxy';
     bootstrap();
diff --git a/lib/util/http/host-rules.ts b/lib/util/http/host-rules.ts
index 23310bf9d2..ede097e2ea 100644
--- a/lib/util/http/host-rules.ts
+++ b/lib/util/http/host-rules.ts
@@ -9,6 +9,7 @@ import { hasProxy } from '../../proxy';
 import type { HostRule } from '../../types';
 import * as hostRules from '../host-rules';
 import { dnsLookup } from './dns';
+import { keepaliveAgents } from './keepalive';
 import type { GotOptions } from './types';
 
 export function findMatchingRules(options: GotOptions, url: string): HostRule {
@@ -109,6 +110,10 @@ export function applyHostRules(url: string, inOptions: GotOptions): GotOptions {
     options.lookup = dnsLookup;
   }
 
+  if (foundRules.keepalive) {
+    options.agent = keepaliveAgents;
+  }
+
   if (!hasProxy() && foundRules.enableHttp2 === true) {
     options.http2 = true;
   }
diff --git a/lib/util/http/keepalive.ts b/lib/util/http/keepalive.ts
new file mode 100644
index 0000000000..abc46edfa1
--- /dev/null
+++ b/lib/util/http/keepalive.ts
@@ -0,0 +1,12 @@
+import Agent, { HttpsAgent } from 'agentkeepalive';
+import type { Agents } from 'got';
+
+const http = new Agent();
+const https = new HttpsAgent();
+
+const keepaliveAgents: Agents = {
+  http,
+  https,
+};
+
+export { keepaliveAgents };
diff --git a/package.json b/package.json
index 5c72cc0b0d..7a16801216 100644
--- a/package.json
+++ b/package.json
@@ -149,6 +149,7 @@
     "@types/tmp": "0.2.3",
     "@yarnpkg/core": "3.2.4",
     "@yarnpkg/parsers": "2.5.1",
+    "agentkeepalive": "4.2.1",
     "auth-header": "1.0.0",
     "azure-devops-node-api": "11.2.0",
     "bunyan": "1.8.15",
diff --git a/yarn.lock b/yarn.lock
index 54900a68f4..1096d93f30 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3172,7 +3172,7 @@ agent-base@6, agent-base@^6.0.2:
   dependencies:
     debug "4"
 
-agentkeepalive@^4.2.1:
+agentkeepalive@4.2.1, agentkeepalive@^4.2.1:
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717"
   integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==
-- 
GitLab