Skip to content

Commit 8363583

Browse files
committed
Add ability to remove keys by type or API call
Fixes falcondev-oss#79 Partially related to falcondev-oss#74 Signed-off-by: Oleksandr Porunov <[email protected]>
1 parent 772e7ac commit 8363583

File tree

5 files changed

+143
-0
lines changed

5 files changed

+143
-0
lines changed

docs/content/1.getting-started/1.index.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,38 @@ The number of days to keep stale cache data and metadata before deleting it. Set
7777

7878
The port the server should listen on.
7979

80+
#### `ENABLE_TYPED_KEY_PREFIX_REMOVAL`
81+
82+
- Default: `false`
83+
84+
If enabled then any specifically structured keys will be automatically removed if there are more keys of such type than
85+
the amount specified via `MAX_STORED_KEYS_PER_TYPE`.
86+
Key type is defined by the following format:
87+
`{key_type}{TYPED_KEY_DELIMITER}{optional custom string}`
88+
89+
Usually, it's recommended to use the following key structure:
90+
`{repository_name}_{branch}_{unique_key_name}{TYPED_KEY_DELIMITER}{hash or other unique data}`
91+
92+
An example could look like:
93+
`falcondev-oss/github-actions-cache-server_dev_docker-cache@#@16ee33c5f9f59c0`
94+
95+
In such case older keys with prefix `falcondev-oss/github-actions-cache-server_dev_docker-cache` will be automatically
96+
removed whenever there are more records for this key type then the one specified via `MAX_STORED_KEYS_PER_TYPE`.
97+
98+
#### `MAX_STORED_KEYS_PER_TYPE`
99+
100+
- Default: `3`
101+
102+
If `ENABLE_TYPED_KEY_PREFIX_REMOVAL` is `true` then this is the maximum amount of the most recent keys to keep per key type.
103+
Any other older keys will be automatically removed.
104+
105+
#### `TYPED_KEY_DELIMITER`
106+
107+
- Default: `@#@`
108+
109+
If `ENABLE_TYPED_KEY_PREFIX_REMOVAL` is `true` then this is the delimiter which is used to identify the key type.
110+
Any prefix before `@#@` is considered to be the key type.
111+
80112
## 2. Setup with Self-Hosted Runners
81113

82114
To leverage the GitHub Actions Cache Server with your self-hosted runners, you'll need to configure a couple of environment variables on your runners. This ensures that your runners can authenticate with and utilize the cache server effectively.
@@ -139,3 +171,22 @@ Doing it this way is a bit of a hack, but it's easier than compiling your own ru
139171
There is no need to change any of your workflows! 🔥
140172

141173
If you've set up your self-hosted runners correctly, they will automatically use the cache server for caching.
174+
175+
## 4. Cache cleanup
176+
177+
Cache cleanup can be triggered automatically via configured eviction policies like:
178+
- `CLEANUP_OLDER_THAN_DAYS` - eviction by time.
179+
- `ENABLE_TYPED_KEY_PREFIX_REMOVAL` - eviction by key type (key prefix).
180+
181+
Moreover, an additional API exists which can be used to evict key records by using specified key prefix.
182+
API path: `/<token>/extra/clear_key_prefix`. API method: `POST`.
183+
Request body: `{ keyPrefix: string }`.
184+
185+
Any keys with the prefix of `keyPrefix` will be removed and data will be cleared for such keys.
186+
Usually, it's useful to call keys removal for an archived branch or a closed PR where you know that the cache won't
187+
be reused anymore. For such cases you could have a step which is called and clears all keys prefixed with the branch name.
188+
For example:
189+
```
190+
- name: Remove keys by prefix
191+
run: curl --header "Content-Type: application/json" --request POST --data '{"keyPrefix":"${{ github.head_ref || github.ref_name }}"}' "${ACTIONS_CACHE_URL}extra/clear_key_prefix"
192+
```

lib/db/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,31 @@ export async function findStaleKeys(
202202
.execute()
203203
}
204204

205+
export async function findPrefixedKeysForRemoval(
206+
db: DB,
207+
{ keyPrefix, skipRecentKeysLimit }: { keyPrefix: string; skipRecentKeysLimit?: number; },
208+
) {
209+
210+
let query = db
211+
.selectFrom('cache_keys')
212+
.where('key', 'like', `${keyPrefix}%`)
213+
214+
if (skipRecentKeysLimit && skipRecentKeysLimit > 0){
215+
query = query.where(({ eb, selectFrom, not }) => not(eb(
216+
'id',
217+
'in',
218+
selectFrom('cache_keys')
219+
.select('cache_keys.id')
220+
.orderBy('cache_keys.accessed_at desc')
221+
.limit(skipRecentKeysLimit)
222+
)))
223+
}
224+
225+
return query
226+
.selectAll()
227+
.execute()
228+
}
229+
205230
export async function createKey(
206231
db: DB,
207232
{ key, version, date }: { key: string; version: string; date?: Date },

lib/env.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ const envSchema = z.object({
1010
STORAGE_DRIVER: z.string().toLowerCase().default('filesystem'),
1111
DB_DRIVER: z.string().toLowerCase().default('sqlite'),
1212
DEBUG: booleanSchema.default('false'),
13+
ENABLE_TYPED_KEY_PREFIX_REMOVAL: booleanSchema.default('false'),
14+
MAX_STORED_KEYS_PER_TYPE: z.coerce.number().int().min(0).default(3),
15+
TYPED_KEY_DELIMITER: z.string().default('@#@'),
1316
})
1417

1518
const parsedEnv = envSchema.safeParse(process.env)

lib/storage/index.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
updateOrCreateKey,
1111
uploadExists,
1212
useDB,
13+
findPrefixedKeysForRemoval,
1314
} from '~/lib/db'
1415
import { ENV } from '~/lib/env'
1516
import { logger } from '~/lib/logger'
@@ -20,6 +21,7 @@ import type { Buffer } from 'node:buffer'
2021
import type { Readable } from 'node:stream'
2122

2223
export interface Storage {
24+
pruneCacheByKeyPrefix: (keyPrefix: string) => Promise<void>
2325
getCacheEntry: (
2426
keys: string[],
2527
version: string,
@@ -63,6 +65,41 @@ export async function initializeStorage() {
6365
const db = useDB()
6466

6567
storage = {
68+
69+
async pruneCacheKeys(keysForRemoval){
70+
if (keysForRemoval.length === 0) {
71+
logger.debug('Prune: No caches to prune')
72+
return
73+
}
74+
75+
await driver.delete({
76+
objectNames: keysForRemoval.map((key) => getObjectNameFromKey(key.key, key.version)),
77+
})
78+
79+
await pruneKeys(db, keysForRemoval)
80+
},
81+
82+
async pruneStaleCacheByType(key){
83+
if (!ENV.ENABLE_TYPED_KEY_PREFIX_REMOVAL){
84+
return
85+
}
86+
let keyTypeIndex = key.indexOf(ENV.TYPED_KEY_DELIMITER)
87+
if (keyTypeIndex < 1){
88+
return
89+
}
90+
let keyType = key.substring(0, keyTypeIndex)
91+
logger.debug(`Prune by type is called: Type [${keyType}]. Full key [${key}].`)
92+
let keysForRemoval = await findPrefixedKeysForRemoval(db, { keyPrefix: keyType, skipRecentKeysLimit: ENV.MAX_STORED_KEYS_PER_TYPE } )
93+
logger.debug(`Removing ${keysForRemoval.length} keys for prefix [${keyType}].`)
94+
await this.pruneCacheKeys(keysForRemoval)
95+
},
96+
97+
async pruneCacheByKeyPrefix(keyPrefix){
98+
let keysForRemoval = await findPrefixedKeysForRemoval(db, { keyPrefix: keyPrefix, skipRecentKeysLimit: 0 } )
99+
logger.debug(`Removing ${keysForRemoval.length} keys for prefix [${keyPrefix}].`)
100+
await this.pruneCacheKeys(keysForRemoval)
101+
},
102+
66103
async reserveCache(key, version, totalSize) {
67104
logger.debug('Reserve:', { key, version })
68105

@@ -97,6 +134,8 @@ export async function initializeStorage() {
97134
uploadId,
98135
})
99136

137+
this.pruneStaleCacheByType(key)
138+
100139
return {
101140
cacheId: uploadId,
102141
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { z } from 'zod'
2+
3+
import { auth } from '~/lib/auth'
4+
import { useStorageAdapter } from '~/lib/storage'
5+
6+
const bodySchema = z.object({
7+
keyPrefix: z.string().min(1),
8+
})
9+
10+
export default defineEventHandler({
11+
onRequest: [auth],
12+
handler: async (event) => {
13+
const body = (await readBody(event)) as unknown
14+
const parsedBody = bodySchema.safeParse(body)
15+
if (!parsedBody.success)
16+
throw createError({
17+
statusCode: 400,
18+
statusMessage: `Invalid body: ${parsedBody.error.message}`,
19+
})
20+
21+
const { keyPrefix } = parsedBody.data
22+
23+
await useStorageAdapter().pruneCacheByKeyPrefix(keyPrefix)
24+
},
25+
})

0 commit comments

Comments
 (0)