From 53fa8cc945e4a7329993b9acdb7a05f10586a9b3 Mon Sep 17 00:00:00 2001
From: Sergei Zharinov <zharinov@users.noreply.github.com>
Date: Tue, 28 Jan 2025 07:15:19 -0300
Subject: [PATCH] refactor(cache): Utils to calculate soft and hard TTL
 (#33844)

---
 lib/config/presets/index.ts                   |  2 +-
 lib/util/cache/package/decorator.ts           | 27 ++++--------
 lib/util/cache/package/ttl.ts                 | 43 +++++++++++++++++++
 .../cache/package-http-cache-provider.spec.ts |  2 +-
 .../http/cache/package-http-cache-provider.ts | 28 ++++++------
 5 files changed, 69 insertions(+), 33 deletions(-)
 create mode 100644 lib/util/cache/package/ttl.ts

diff --git a/lib/config/presets/index.ts b/lib/config/presets/index.ts
index 349db47089..b482521ff4 100644
--- a/lib/config/presets/index.ts
+++ b/lib/config/presets/index.ts
@@ -7,7 +7,7 @@ import { logger } from '../../logger';
 import { ExternalHostError } from '../../types/errors/external-host-error';
 import * as memCache from '../../util/cache/memory';
 import * as packageCache from '../../util/cache/package';
-import { getTtlOverride } from '../../util/cache/package/decorator';
+import { getTtlOverride } from '../../util/cache/package/ttl';
 import { clone } from '../../util/clone';
 import { regEx } from '../../util/regex';
 import * as template from '../../util/template';
diff --git a/lib/util/cache/package/decorator.ts b/lib/util/cache/package/decorator.ts
index 70be6ff6d0..ee7424ffe6 100644
--- a/lib/util/cache/package/decorator.ts
+++ b/lib/util/cache/package/decorator.ts
@@ -5,6 +5,7 @@ import { logger } from '../../../logger';
 import type { Decorator } from '../../decorator';
 import { decorate } from '../../decorator';
 import { acquireLock } from '../../mutex';
+import { resolveTtlValues } from './ttl';
 import type { DecoratorCachedRecord, PackageCacheNamespace } from './types';
 import * as packageCache from '.';
 
@@ -91,17 +92,13 @@ export function cache<T>({
         finalKey,
       );
 
-      const ttlOverride = getTtlOverride(finalNamespace);
-      const softTtl = ttlOverride ?? ttlMinutes;
-
-      const cacheHardTtlMinutes = GlobalConfig.get(
-        'cacheHardTtlMinutes',
-        7 * 24 * 60,
-      );
-      let hardTtl = softTtl;
-      if (methodName === 'getReleases' || methodName === 'getDigest') {
-        hardTtl = Math.max(softTtl, cacheHardTtlMinutes);
-      }
+      const ttlValues = resolveTtlValues(finalNamespace, ttlMinutes);
+      const softTtl = ttlValues.softTtlMinutes;
+      const hardTtl =
+        methodName === 'getReleases' || methodName === 'getDigest'
+          ? ttlValues.hardTtlMinutes
+          : // Skip two-tier TTL for any intermediate data fetching
+            softTtl;
 
       let oldData: unknown;
       if (oldRecord) {
@@ -148,11 +145,3 @@ export function cache<T>({
     }
   });
 }
-
-export function getTtlOverride(namespace: string): number | undefined {
-  const ttl: unknown = GlobalConfig.get('cacheTtlOverride', {})[namespace];
-  if (is.number(ttl)) {
-    return ttl;
-  }
-  return undefined;
-}
diff --git a/lib/util/cache/package/ttl.ts b/lib/util/cache/package/ttl.ts
new file mode 100644
index 0000000000..5ed8f0b523
--- /dev/null
+++ b/lib/util/cache/package/ttl.ts
@@ -0,0 +1,43 @@
+import is from '@sindresorhus/is';
+import { GlobalConfig } from '../../../config/global';
+import type { PackageCacheNamespace } from './types';
+
+export function getTtlOverride(
+  namespace: PackageCacheNamespace,
+): number | undefined {
+  const ttl = GlobalConfig.get('cacheTtlOverride', {})[namespace];
+  if (is.number(ttl)) {
+    return ttl;
+  }
+  return undefined;
+}
+
+export interface TTLValues {
+  /** TTL for serving cached value without hitting the server */
+  softTtlMinutes: number;
+
+  /** TTL for serving stale cache when upstream responds with errors */
+  hardTtlMinutes: number;
+}
+
+/**
+ * Apply user-configured overrides and return the final values for soft/hard TTL.
+ *
+ * @param namespace Cache namespace
+ * @param ttlMinutes TTL value configured in Renovate codebase
+ * @returns
+ */
+export function resolveTtlValues(
+  namespace: PackageCacheNamespace,
+  ttlMinutes: number,
+): TTLValues {
+  const softTtlMinutes = getTtlOverride(namespace) ?? ttlMinutes;
+
+  const cacheHardTtlMinutes = GlobalConfig.get(
+    'cacheHardTtlMinutes',
+    7 * 24 * 60,
+  );
+  const hardTtlMinutes = Math.max(softTtlMinutes, cacheHardTtlMinutes);
+
+  return { softTtlMinutes, hardTtlMinutes };
+}
diff --git a/lib/util/http/cache/package-http-cache-provider.spec.ts b/lib/util/http/cache/package-http-cache-provider.spec.ts
index 00c5121ed4..3b01971528 100644
--- a/lib/util/http/cache/package-http-cache-provider.spec.ts
+++ b/lib/util/http/cache/package-http-cache-provider.spec.ts
@@ -50,7 +50,7 @@ describe('util/http/cache/package-http-cache-provider', () => {
     };
     const cacheProvider = new PackageHttpCacheProvider({
       namespace: '_test-namespace',
-      softTtlMinutes: 0,
+      ttlMinutes: 0,
     });
     httpMock.scope(url).get('').reply(200, 'new response');
 
diff --git a/lib/util/http/cache/package-http-cache-provider.ts b/lib/util/http/cache/package-http-cache-provider.ts
index 8d36bc110e..6b79e9ce42 100644
--- a/lib/util/http/cache/package-http-cache-provider.ts
+++ b/lib/util/http/cache/package-http-cache-provider.ts
@@ -1,30 +1,32 @@
 import { DateTime } from 'luxon';
 import { get, set } from '../../cache/package'; // Import the package cache functions
+import { resolveTtlValues } from '../../cache/package/ttl';
 import type { PackageCacheNamespace } from '../../cache/package/types';
+import { HttpCacheStats } from '../../stats';
 import type { HttpResponse } from '../types';
 import { AbstractHttpCacheProvider } from './abstract-http-cache-provider';
 import type { HttpCache } from './schema';
 
 export interface PackageHttpCacheProviderOptions {
   namespace: PackageCacheNamespace;
-  softTtlMinutes?: number;
-  hardTtlMinutes?: number;
+  ttlMinutes?: number;
 }
 
 export class PackageHttpCacheProvider extends AbstractHttpCacheProvider {
   private namespace: PackageCacheNamespace;
-  private softTtlMinutes = 15;
-  private hardTtlMinutes = 24 * 60;
-
-  constructor({
-    namespace,
-    softTtlMinutes,
-    hardTtlMinutes,
-  }: PackageHttpCacheProviderOptions) {
+
+  private softTtlMinutes: number;
+  private hardTtlMinutes: number;
+
+  constructor({ namespace, ttlMinutes = 15 }: PackageHttpCacheProviderOptions) {
     super();
     this.namespace = namespace;
-    this.softTtlMinutes = softTtlMinutes ?? this.softTtlMinutes;
-    this.hardTtlMinutes = hardTtlMinutes ?? this.hardTtlMinutes;
+    const { softTtlMinutes, hardTtlMinutes } = resolveTtlValues(
+      this.namespace,
+      ttlMinutes,
+    );
+    this.softTtlMinutes = softTtlMinutes;
+    this.hardTtlMinutes = hardTtlMinutes;
   }
 
   async load(url: string): Promise<unknown> {
@@ -47,9 +49,11 @@ export class PackageHttpCacheProvider extends AbstractHttpCacheProvider {
     const deadline = cachedAt.plus({ minutes: this.softTtlMinutes });
     const now = DateTime.now();
     if (now >= deadline) {
+      HttpCacheStats.incLocalMisses(url);
       return null;
     }
 
+    HttpCacheStats.incLocalHits(url);
     return cached.httpResponse as HttpResponse<T>;
   }
 }
-- 
GitLab