From 51a43d5e41e0727b618553a688ff554898677d66 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Mon, 4 May 2020 08:27:38 +0200
Subject: [PATCH] feat(internal): cache extractions results (#6118)

---
 .../extract/__snapshots__/cache.spec.ts.snap  |  3 +
 lib/workers/repository/extract/cache.spec.ts  | 30 +++++++++
 lib/workers/repository/extract/cache.ts       | 67 +++++++++++++++++++
 lib/workers/repository/extract/index.spec.ts  |  8 +++
 lib/workers/repository/extract/index.ts       |  6 ++
 .../onboarding/branch/index.spec.ts           |  1 +
 6 files changed, 115 insertions(+)
 create mode 100644 lib/workers/repository/extract/__snapshots__/cache.spec.ts.snap
 create mode 100644 lib/workers/repository/extract/cache.spec.ts
 create mode 100644 lib/workers/repository/extract/cache.ts

diff --git a/lib/workers/repository/extract/__snapshots__/cache.spec.ts.snap b/lib/workers/repository/extract/__snapshots__/cache.spec.ts.snap
new file mode 100644
index 0000000000..649f7005f5
--- /dev/null
+++ b/lib/workers/repository/extract/__snapshots__/cache.spec.ts.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workers/repository/extract/cache returns a hash 1`] = `"3Jfx5tzsGJEm8HQCeHT/6QL5vzE="`;
diff --git a/lib/workers/repository/extract/cache.spec.ts b/lib/workers/repository/extract/cache.spec.ts
new file mode 100644
index 0000000000..18b16bfa85
--- /dev/null
+++ b/lib/workers/repository/extract/cache.spec.ts
@@ -0,0 +1,30 @@
+import * as cache from './cache';
+
+describe('workers/repository/extract/cache', () => {
+  const config = { baseBranch: 'master', baseBranchSha: 'abc123' };
+  const extractList = [];
+  const extraction = { foo: [] };
+  it('handles missing sha', () => {
+    expect(cache.getExtractHash({}, [])).toBeNull();
+  });
+  it('returns a hash', () => {
+    expect(
+      cache.getExtractHash({ baseBranchSha: 'abc123' }, [])
+    ).toMatchSnapshot();
+  });
+  it('sets a value', async () => {
+    await cache.setCachedExtract(config, extractList, extraction);
+  });
+  it('gets no value', async () => {
+    extractList.push('abc');
+    const res = await cache.getCachedExtract(config, extractList);
+    expect(res).toEqual(null);
+  });
+  it('handles no sha error', async () => {
+    const res = await cache.getCachedExtract(
+      { baseBranch: 'nothing' },
+      extractList
+    );
+    expect(res).toBeNull();
+  });
+});
diff --git a/lib/workers/repository/extract/cache.ts b/lib/workers/repository/extract/cache.ts
new file mode 100644
index 0000000000..ca4baedcb7
--- /dev/null
+++ b/lib/workers/repository/extract/cache.ts
@@ -0,0 +1,67 @@
+import crypto from 'crypto';
+import { RenovateConfig } from '../../../config/common';
+import { logger } from '../../../logger';
+import { PackageFile } from '../../../manager/common';
+
+function getCacheNamespaceKey(
+  config: RenovateConfig
+): { cacheNamespace: string; cacheKey: string } {
+  // Cache extract results per-base branch
+  const { platform, repository, baseBranch } = config;
+  const cacheNamespace = 'repository-extract';
+  const cacheKey = `${platform}/${repository}/${baseBranch}`;
+  return { cacheNamespace, cacheKey };
+}
+
+export function getExtractHash(
+  config: RenovateConfig,
+  extractList: RenovateConfig[]
+): string | null {
+  // A cache is only valid if the following are unchanged:
+  //  * base branch SHA
+  //  * the list of matching files for each manager
+  if (!config.baseBranchSha) {
+    logger.warn('No baseBranchSha found in config');
+    return null;
+  }
+  return crypto
+    .createHash('sha1')
+    .update(config.baseBranchSha)
+    .update(JSON.stringify(extractList))
+    .digest('base64');
+}
+
+export async function getCachedExtract(
+  config: RenovateConfig,
+  extractList: RenovateConfig[]
+): Promise<Record<string, PackageFile[]> | null> {
+  const { baseBranch } = config;
+  const { cacheNamespace, cacheKey } = getCacheNamespaceKey(config);
+  const cachedExtract = await renovateCache.get(cacheNamespace, cacheKey);
+  // istanbul ignore if
+  if (cachedExtract) {
+    const extractHash = getExtractHash(config, extractList);
+    if (cachedExtract.extractHash === extractHash) {
+      logger.info({ baseBranch }, 'Returning cached extract result');
+      return cachedExtract.extractions;
+    }
+    logger.debug({ baseBranch }, 'Cached extract result does not match');
+  } else {
+    logger.debug({ baseBranch }, 'No cached extract result found');
+  }
+  return null;
+}
+
+export async function setCachedExtract(
+  config: RenovateConfig,
+  extractList: RenovateConfig[],
+  extractions: Record<string, PackageFile[]>
+): Promise<void> {
+  const { baseBranch } = config;
+  logger.debug({ baseBranch }, 'Setting cached extract result');
+  const { cacheNamespace, cacheKey } = getCacheNamespaceKey(config);
+  const extractHash = getExtractHash(config, extractList);
+  const payload = { extractHash, extractions };
+  const cacheMinutes = 24 * 60;
+  await renovateCache.set(cacheNamespace, cacheKey, payload, cacheMinutes);
+}
diff --git a/lib/workers/repository/extract/index.spec.ts b/lib/workers/repository/extract/index.spec.ts
index 37087657b6..45a461a9d6 100644
--- a/lib/workers/repository/extract/index.spec.ts
+++ b/lib/workers/repository/extract/index.spec.ts
@@ -1,10 +1,13 @@
 import { defaultConfig, mocked, platform } from '../../../../test/util';
 import { RenovateConfig } from '../../../config';
+import * as _cache from './cache';
 import * as _managerFiles from './manager-files';
 import { extractAllDependencies } from '.';
 
+jest.mock('./cache');
 jest.mock('./manager-files');
 
+const cache = mocked(_cache);
 const managerFiles = mocked(_managerFiles);
 
 describe('workers/repository/extract/index', () => {
@@ -21,6 +24,11 @@ describe('workers/repository/extract/index', () => {
       const res = await extractAllDependencies(config);
       expect(Object.keys(res).includes('ansible')).toBe(true);
     });
+    it('uses cache', async () => {
+      cache.getCachedExtract.mockResolvedValueOnce({} as never);
+      const res = await extractAllDependencies(config);
+      expect(res).toEqual({});
+    });
     it('skips non-enabled managers', async () => {
       config.enabledManagers = ['npm'];
       managerFiles.getManagerPackageFiles.mockResolvedValue([{} as never]);
diff --git a/lib/workers/repository/extract/index.ts b/lib/workers/repository/extract/index.ts
index a27d8efec0..5fde50312d 100644
--- a/lib/workers/repository/extract/index.ts
+++ b/lib/workers/repository/extract/index.ts
@@ -7,6 +7,7 @@ import {
 import { logger } from '../../../logger';
 import { getManagerList } from '../../../manager';
 import { PackageFile } from '../../../manager/common';
+import { getCachedExtract, setCachedExtract } from './cache';
 import { getMatchingFiles } from './file-match';
 import { getManagerPackageFiles } from './manager-files';
 
@@ -44,6 +45,10 @@ export async function extractAllDependencies(
       }
     }
   }
+  const cachedExtractions = await getCachedExtract(config, extractList);
+  if (cachedExtractions) {
+    return cachedExtractions;
+  }
   const extractResults = await Promise.all(
     extractList.map(async (managerConfig) => {
       const packageFiles = await getManagerPackageFiles(managerConfig);
@@ -60,5 +65,6 @@ export async function extractAllDependencies(
     }
   }
   logger.debug(`Found ${fileCount} package file(s)`);
+  await setCachedExtract(config, extractList, extractions);
   return extractions;
 }
diff --git a/lib/workers/repository/onboarding/branch/index.spec.ts b/lib/workers/repository/onboarding/branch/index.spec.ts
index 1ffea415f2..a8a4090537 100644
--- a/lib/workers/repository/onboarding/branch/index.spec.ts
+++ b/lib/workers/repository/onboarding/branch/index.spec.ts
@@ -8,6 +8,7 @@ import { checkOnboardingBranch } from '.';
 const rebase: any = _rebase;
 
 jest.mock('../../../../workers/repository/onboarding/branch/rebase');
+jest.mock('../../extract/cache');
 
 describe('workers/repository/onboarding/branch', () => {
   describe('checkOnboardingBranch', () => {
-- 
GitLab