Skip to content

Commit 922234e

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 e83e1ca commit 922234e

File tree

5 files changed

+145
-2
lines changed

5 files changed

+145
-2
lines changed

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,38 @@ The port the server should listen on.
8989

9090
The directory to use for temporary files.
9191

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

94126
Set the `ACTIONS_RESULTS_URL` on your runner to the API URL (with a trailing slash).
@@ -148,3 +180,22 @@ variant: subtle
148180
There is no need to change any of your workflows! 🔥
149181

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

lib/db/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,31 @@ export async function findStaleKeys(
221221
.execute()
222222
}
223223

224+
export async function findPrefixedKeysForRemoval(
225+
db: DB,
226+
{ keyPrefix, skipRecentKeysLimit }: { keyPrefix: string; skipRecentKeysLimit?: number; },
227+
) {
228+
229+
let query = db
230+
.selectFrom('cache_keys')
231+
.where('key', 'like', `${keyPrefix}%`)
232+
233+
if (skipRecentKeysLimit && skipRecentKeysLimit > 0){
234+
query = query.where(({ eb, selectFrom, not }) => not(eb(
235+
'id',
236+
'in',
237+
selectFrom('cache_keys')
238+
.select('cache_keys.id')
239+
.orderBy('cache_keys.accessed_at desc')
240+
.limit(skipRecentKeysLimit)
241+
)))
242+
}
243+
244+
return query
245+
.selectAll()
246+
.execute()
247+
}
248+
224249
export async function createKey(
225250
db: DB,
226251
{ 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
@@ -15,6 +15,9 @@ const envSchema = z.object({
1515
DEBUG: booleanSchema.default('false'),
1616
NITRO_PORT: portSchema.default(3000),
1717
TEMP_DIR: z.string().default(tmpdir()),
18+
ENABLE_TYPED_KEY_PREFIX_REMOVAL: booleanSchema.default('false'),
19+
MAX_STORED_KEYS_PER_TYPE: z.coerce.number().int().min(0).default(3),
20+
TYPED_KEY_DELIMITER: z.string().default('@#@'),
1821
})
1922

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

lib/storage/index.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
touchKey,
1414
updateOrCreateKey,
1515
useDB,
16+
findPrefixedKeysForRemoval,
1617
} from '~/lib/db'
1718
import { ENV } from '~/lib/env'
1819
import { logger } from '~/lib/logger'
@@ -21,6 +22,7 @@ import { getStorageDriver } from '~/lib/storage/drivers'
2122
import { getObjectNameFromKey } from '~/lib/utils'
2223

2324
export interface Storage {
25+
pruneCacheByKeyPrefix: (keyPrefix: string) => Promise<void>
2426
getCacheEntry: (
2527
keys: string[],
2628
version: string,
@@ -68,9 +70,44 @@ export async function initializeStorage() {
6870
const driver = await driverSetup()
6971
const db = await useDB()
7072

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

75112
if (await getUpload(db, { key, version })) {
76113
logger.debug(`Reserve: Already reserved. Ignoring...`, { key, version })
@@ -103,6 +140,8 @@ export async function initializeStorage() {
103140
uploadId,
104141
})
105142

143+
this.pruneStaleCacheByType(key)
144+
106145
return {
107146
cacheId: uploadId,
108147
}
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 { useStorageAdapter } from '~/lib/storage'
4+
5+
const queryParamSchema = z.object({
6+
keyPrefix: z.string().min(1),
7+
})
8+
9+
export default defineEventHandler(async (event) => {
10+
const parsedQuery = queryParamSchema.safeParse(getQuery(event))
11+
if (!parsedQuery.success)
12+
throw createError({
13+
statusCode: 400,
14+
statusMessage: `Invalid query parameters: ${parsedQuery.error.message}`,
15+
})
16+
17+
const { keyPrefix } = parsedQuery.data
18+
19+
const adapter = await useStorageAdapter()
20+
await adapter.pruneCacheByKeyPrefix(keyPrefix)
21+
22+
return {
23+
ok: true,
24+
}
25+
})

0 commit comments

Comments
 (0)