Skip to content

Commit d7bf11b

Browse files
committed
feat(readme-backend): added config to set the cache ttl
1 parent 023259c commit d7bf11b

9 files changed

Lines changed: 170 additions & 17 deletions

File tree

.changeset/many-baboons-lick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@axis-backstage/plugin-readme-backend': patch
3+
---
4+
5+
Added config to set the cache ttl.

plugins/readme-backend/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@ readme:
4646
- README.markdown
4747
```
4848
49+
To override the default TTL of the internal cache a duration can be set using the `cacheTtl` key. This can be either a string
50+
or an object. The following examples should be equal.
51+
52+
```yaml
53+
readme:
54+
cacheTtl:
55+
hours: 2
56+
```
57+
58+
```yaml
59+
readme:
60+
cacheTtl: '2h'
61+
```
62+
4963
### Troubleshooting
5064

5165
If the backend fails to provide README content for an entity, it could be due to several reasons.

plugins/readme-backend/config.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { HumanDuration } from '@backstage/types';
2+
13
export interface Config {
24
/**
35
* Configuration options for the Readme backend plugin
@@ -8,5 +10,9 @@ export interface Config {
810
* when looking for a README file and which order to use.
911
*/
1012
fileNames?: string[];
13+
/**
14+
* Optional cache TTL, defaults to 1 hour.
15+
*/
16+
cacheTtl?: HumanDuration | string;
1117
};
1218
}

plugins/readme-backend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,17 @@
3232
"@backstage/backend-plugin-api": "^1.2.0",
3333
"@backstage/catalog-client": "^1.9.1",
3434
"@backstage/catalog-model": "^1.7.3",
35+
"@backstage/config": "^1.3.2",
3536
"@backstage/errors": "^1.2.7",
3637
"@backstage/integration": "^1.16.1",
38+
"@backstage/types": "^1.2.1",
3739
"@types/express": "*",
3840
"express": "^4.17.1",
3941
"express-promise-router": "^4.1.0"
4042
},
4143
"devDependencies": {
4244
"@backstage/backend-test-utils": "^1.3.0",
4345
"@backstage/cli": "^0.30.0",
44-
"@backstage/config": "^1.3.2",
4546
"@types/supertest": "^2.0.12",
4647
"msw": "^2.7.3",
4748
"supertest": "^6.2.4"
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { mockServices } from '@backstage/backend-test-utils';
2+
3+
import { getCacheTtl } from './config';
4+
5+
describe('getCacheTtl', () => {
6+
it('should return default TTL when cache.ttl is not configured', () => {
7+
const mockConfig = mockServices.rootConfig({
8+
data: {},
9+
});
10+
11+
const result = getCacheTtl(mockConfig);
12+
13+
expect(result).toBe(3_600_000); // 1 hour in milliseconds
14+
});
15+
16+
it('should return default TTL when readme config section exists but no ttl key', () => {
17+
const mockConfig = mockServices.rootConfig({
18+
data: {
19+
readme: {},
20+
},
21+
});
22+
23+
const result = getCacheTtl(mockConfig);
24+
25+
expect(result).toBe(3_600_000); // 1 hour in milliseconds
26+
});
27+
28+
it('should return configured TTL in seconds', () => {
29+
const mockConfig = mockServices.rootConfig({
30+
data: {
31+
readme: {
32+
cacheTtl: { seconds: 1800 }, // 30 minutes
33+
},
34+
},
35+
});
36+
37+
const result = getCacheTtl(mockConfig);
38+
39+
expect(result).toBe(1_800_000); // 30 minutes in milliseconds
40+
});
41+
42+
it('should return configured TTL in minutes', () => {
43+
const mockConfig = mockServices.rootConfig({
44+
data: {
45+
readme: {
46+
cacheTtl: { minutes: 10 },
47+
},
48+
},
49+
});
50+
51+
const result = getCacheTtl(mockConfig);
52+
53+
expect(result).toBe(600_000); // 10 minutes in milliseconds
54+
});
55+
56+
it('should return configured TTL in hours', () => {
57+
const mockConfig = mockServices.rootConfig({
58+
data: {
59+
readme: {
60+
cacheTtl: { hours: 2 },
61+
},
62+
},
63+
});
64+
65+
const result = getCacheTtl(mockConfig);
66+
67+
expect(result).toBe(7_200_000); // 2 hours in milliseconds
68+
});
69+
70+
it('should return configured TTL as a string value (interpreted as milliseconds)', () => {
71+
const mockConfig = mockServices.rootConfig({
72+
data: {
73+
readme: {
74+
cacheTtl: '4w',
75+
},
76+
},
77+
});
78+
79+
const result = getCacheTtl(mockConfig);
80+
81+
expect(result).toBe(2419200000); // 4 weeks in milliseconds
82+
});
83+
84+
it('should handle complex duration configuration', () => {
85+
const mockConfig = mockServices.rootConfig({
86+
data: {
87+
readme: {
88+
cacheTtl: {
89+
hours: 1,
90+
minutes: 30,
91+
seconds: 45,
92+
},
93+
},
94+
},
95+
});
96+
97+
const result = getCacheTtl(mockConfig);
98+
99+
expect(result).toBe(5_445_000); // 1h 30m 45s in milliseconds
100+
});
101+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Config, readDurationFromConfig } from '@backstage/config';
2+
import { durationToMilliseconds } from '@backstage/types';
3+
4+
const CACHE_CONFIG_KEY = 'readme.cacheTtl';
5+
6+
export const getCacheTtl = (config: Config): number =>
7+
config.has(CACHE_CONFIG_KEY)
8+
? durationToMilliseconds(
9+
readDurationFromConfig(config, { key: CACHE_CONFIG_KEY }),
10+
)
11+
: 3_600_000;

plugins/readme-backend/src/service/constants.test.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ConfigReader } from '@backstage/config';
1+
import { mockServices } from '@backstage/backend-test-utils';
22
import { NOT_FOUND_PLACEHOLDER, getReadmeTypes } from './constants';
33

44
describe('constants', () => {
@@ -11,7 +11,9 @@ describe('constants', () => {
1111

1212
describe('getReadmeTypes', () => {
1313
it('should return default readme types when config is empty', () => {
14-
const config = new ConfigReader({});
14+
const config = mockServices.rootConfig({
15+
data: {},
16+
});
1517
const readmeTypes = getReadmeTypes(config);
1618
expect(readmeTypes).toEqual([
1719
{ name: 'README.md', type: 'text/markdown' },
@@ -23,9 +25,11 @@ describe('constants', () => {
2325
});
2426

2527
it('should return custom readme types from config', () => {
26-
const config = new ConfigReader({
27-
readme: {
28-
fileNames: ['CUSTOM.md', 'CUSTOM.txt'],
28+
const config = mockServices.rootConfig({
29+
data: {
30+
readme: {
31+
fileNames: ['CUSTOM.md', 'CUSTOM.txt'],
32+
},
2933
},
3034
});
3135
const readmeTypes = getReadmeTypes(config);

plugins/readme-backend/src/service/router.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { isError, NotFoundError } from '@backstage/errors';
1717
import express from 'express';
1818
import Router from 'express-promise-router';
1919
import { isSymLink } from '../lib';
20+
import { getCacheTtl } from './config';
2021
import { NOT_FOUND_PLACEHOLDER, getReadmeTypes } from './constants';
2122
import { ReadmeFile } from './types';
2223

@@ -42,8 +43,9 @@ export async function createRouter(
4243
): Promise<express.Router> {
4344
const { auth, logger, config, reader, discovery, cache } = options;
4445
const catalogClient = new CatalogClient({ discoveryApi: discovery });
46+
const cacheTtl = getCacheTtl(config);
4547

46-
logger.info('Initializing readme backend');
48+
logger.info(`Initializing readme backend. Cache TTL: ${cacheTtl}ms`);
4749
const integrations = ScmIntegrations.fromConfig(config);
4850
const router = Router();
4951
router.use(express.json());
@@ -121,11 +123,15 @@ export async function createRouter(
121123
content = (await symLinkUrlResponse.buffer()).toString('utf-8');
122124
}
123125

124-
cache.set(entityRef, {
125-
name: fileType.name,
126-
type: fileType.type,
127-
content: content,
128-
});
126+
cache.set(
127+
entityRef,
128+
{
129+
name: fileType.name,
130+
type: fileType.type,
131+
content: content,
132+
},
133+
{ ttl: cacheTtl },
134+
);
129135
logger.info(
130136
`Found README for ${entityRef}: ${url} type ${fileType.type}`,
131137
);
@@ -143,11 +149,15 @@ export async function createRouter(
143149
}
144150
}
145151
logger.info(`Readme not found for ${entityRef}`);
146-
cache.set(entityRef, {
147-
name: NOT_FOUND_PLACEHOLDER,
148-
type: '',
149-
content: '',
150-
});
152+
cache.set(
153+
entityRef,
154+
{
155+
name: NOT_FOUND_PLACEHOLDER,
156+
type: '',
157+
content: '',
158+
},
159+
{ ttl: cacheTtl },
160+
);
151161
throw new NotFoundError('Readme could not be found');
152162
});
153163

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1551,6 +1551,7 @@ __metadata:
15511551
"@backstage/config": "npm:^1.3.2"
15521552
"@backstage/errors": "npm:^1.2.7"
15531553
"@backstage/integration": "npm:^1.16.1"
1554+
"@backstage/types": "npm:^1.2.1"
15541555
"@types/express": "npm:*"
15551556
"@types/supertest": "npm:^2.0.12"
15561557
express: "npm:^4.17.1"

0 commit comments

Comments
 (0)