Skip to content

Commit 332cfae

Browse files
committed
feat: make cache server stateless & native multipart uploads
1 parent a4f7b0c commit 332cfae

36 files changed

+2088
-493
lines changed

.env

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@ STORAGE_FILESYSTEM_PATH=.data/storage/filesystem
99

1010
# s3
1111
# STORAGE_DRIVER=s3
12-
# STORAGE_S3_BUCKET=
13-
# STORAGE_S3_ENDPOINT=
14-
# STORAGE_S3_REGION=
15-
# STORAGE_S3_PORT=
16-
# STORAGE_S3_USE_SSL=
17-
# STORAGE_S3_ACCESS_KEY=
18-
# STORAGE_S3_SECRET_KEY=
12+
# STORAGE_S3_BUCKET=test
13+
# STORAGE_S3_ENDPOINT=localhost
14+
# STORAGE_S3_PORT=9000
15+
# STORAGE_S3_USE_SSL=false
16+
# STORAGE_S3_ACCESS_KEY=minioadmin
17+
# STORAGE_S3_SECRET_KEY=minioadmin
1918

2019
# sqlite
2120
DB_DRIVER=sqlite

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM node:20-alpine as builder
1+
FROM node:22-alpine as builder
22

33
ARG BUILD_HASH
44
ENV BUILD_HASH=${BUILD_HASH}
@@ -21,7 +21,7 @@ RUN pnpm run build
2121

2222
# --------------------------------------------
2323

24-
FROM node:20-alpine as runner
24+
FROM node:22-alpine as runner
2525

2626
WORKDIR /app
2727

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default eslintConfig({
1414
.append({
1515
rules: {
1616
'compat/compat': 'off',
17+
'node/prefer-global/buffer': 'off',
1718
},
1819
})
1920
.append({

lib/db/driver.ts renamed to lib/db/defineDatabaseDriver.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable unicorn/filename-case */
12
import { formatZodError } from '~/lib/env'
23
import { logger } from '~/lib/logger'
34

db-drivers/index.ts renamed to lib/db/drivers/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { mysqlDriver } from '~/db-drivers/mysql'
2-
import { postgresDriver } from '~/db-drivers/postgres'
3-
import { sqliteDriver } from '~/db-drivers/sqlite'
4-
import type { defineDatabaseDriver } from '~/lib/db/driver'
1+
import type { defineDatabaseDriver } from '~/lib/db/defineDatabaseDriver'
2+
import { mysqlDriver } from '~/lib/db/drivers/mysql'
3+
import { postgresDriver } from '~/lib/db/drivers/postgres'
4+
import { sqliteDriver } from '~/lib/db/drivers/sqlite'
55

66
const databaseDrivers = {
77
sqlite: sqliteDriver,

db-drivers/mysql.ts renamed to lib/db/drivers/mysql.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { MysqlDialect } from 'kysely'
22
import { createPool } from 'mysql2'
33
import { z } from 'zod'
44

5-
import { defineDatabaseDriver } from '~/lib/db/driver'
5+
import { defineDatabaseDriver } from '~/lib/db/defineDatabaseDriver'
66

77
export const mysqlDriver = defineDatabaseDriver({
88
envSchema: z.object({

db-drivers/postgres.ts renamed to lib/db/drivers/postgres.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { PostgresDialect } from 'kysely'
22
import pg from 'pg'
33
import { z } from 'zod'
44

5-
import { defineDatabaseDriver } from '~/lib/db/driver'
5+
import { defineDatabaseDriver } from '~/lib/db/defineDatabaseDriver'
66

77
export const postgresDriver = defineDatabaseDriver({
88
envSchema: z.object({

db-drivers/sqlite.ts renamed to lib/db/drivers/sqlite.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import SQLite from 'better-sqlite3'
55
import { SqliteDialect } from 'kysely'
66
import { z } from 'zod'
77

8-
import { defineDatabaseDriver } from '~/lib/db/driver'
8+
import { defineDatabaseDriver } from '~/lib/db/defineDatabaseDriver'
99

1010
export const sqliteDriver = defineDatabaseDriver({
1111
envSchema: z.object({

lib/db/index.ts

Lines changed: 80 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,39 @@
11
import { Kysely, Migrator } from 'kysely'
22

3-
import type { DatabaseDriverName } from '~/db-drivers'
4-
import { getDatabaseDriver } from '~/db-drivers'
3+
import type { DatabaseDriverName } from '~/lib/db/drivers'
4+
import { getDatabaseDriver } from '~/lib/db/drivers'
55
import { migrations } from '~/lib/db/migrations'
66
import { ENV } from '~/lib/env'
77
import { logger } from '~/lib/logger'
88

99
import type { Selectable } from 'kysely'
1010

11-
export interface Database {
12-
cache_keys: CacheKeysTable
13-
}
14-
1511
export interface CacheKeysTable {
1612
key: string
1713
version: string
1814
updated_at: string
1915
accessed_at: string
2016
}
17+
export interface UploadsTable {
18+
created_at: string
19+
key: string
20+
version: string
21+
id: number
22+
driver_upload_id: string
23+
}
24+
export interface UploadPartsTable {
25+
upload_id: number
26+
part_number: number
27+
e_tag: string | null
28+
}
29+
30+
export interface Database {
31+
cache_keys: CacheKeysTable
32+
uploads: UploadsTable
33+
upload_parts: UploadPartsTable
34+
}
2135

22-
let db: Kysely<Database>
36+
let _db: Kysely<Database>
2337

2438
export async function initializeDatabase() {
2539
const driverName = ENV.DB_DRIVER
@@ -33,13 +47,13 @@ export async function initializeDatabase() {
3347

3448
const driver = await driverSetup()
3549

36-
db = new Kysely<Database>({
50+
_db = new Kysely<Database>({
3751
dialect: driver,
3852
})
3953

4054
logger.info('Migrating database...')
4155
const migrator = new Migrator({
42-
db,
56+
db: _db,
4357
provider: {
4458
async getMigrations() {
4559
return migrations(driverName as DatabaseDriverName)
@@ -56,15 +70,24 @@ export async function initializeDatabase() {
5670
logger.success('Database migrated')
5771
}
5872

73+
export function useDB() {
74+
return _db
75+
}
76+
77+
type DB = typeof _db
78+
5979
/**
6080
* @see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#matching-a-cache-key
6181
*/
62-
export async function findKeyMatch(opts: { key: string; version: string; restoreKeys?: string[] }) {
63-
logger.debug('Finding key match', opts)
82+
export async function findKeyMatch(
83+
db: DB,
84+
args: { key: string; version: string; restoreKeys?: string[] },
85+
) {
86+
logger.debug('Finding key match', args)
6487
const exactPrimaryMatch = await db
6588
.selectFrom('cache_keys')
66-
.where('key', '=', opts.key)
67-
.where('version', '=', opts.version)
89+
.where('key', '=', args.key)
90+
.where('version', '=', args.version)
6891
.selectAll()
6992
.executeTakeFirst()
7093
if (exactPrimaryMatch) {
@@ -75,8 +98,8 @@ export async function findKeyMatch(opts: { key: string; version: string; restore
7598

7699
const prefixedPrimaryMatch = await db
77100
.selectFrom('cache_keys')
78-
.where('key', 'like', `${opts.key}%`)
79-
.where('version', '=', opts.version)
101+
.where('key', 'like', `${args.key}%`)
102+
.where('version', '=', args.version)
80103
.orderBy('cache_keys.updated_at desc')
81104
.selectAll()
82105
.executeTakeFirst()
@@ -85,16 +108,16 @@ export async function findKeyMatch(opts: { key: string; version: string; restore
85108
return prefixedPrimaryMatch
86109
}
87110

88-
if (!opts.restoreKeys) {
111+
if (!args.restoreKeys) {
89112
logger.debug('No restore keys provided')
90113
return
91114
}
92115

93-
logger.debug('Trying restore keys', opts.restoreKeys)
94-
for (const key of opts.restoreKeys) {
116+
logger.debug('Trying restore keys', args.restoreKeys)
117+
for (const key of args.restoreKeys) {
95118
const exactMatch = await db
96119
.selectFrom('cache_keys')
97-
.where('version', '=', opts.version)
120+
.where('version', '=', args.version)
98121
.where('key', '=', key)
99122
.orderBy('cache_keys.updated_at desc')
100123
.selectAll()
@@ -107,7 +130,7 @@ export async function findKeyMatch(opts: { key: string; version: string; restore
107130

108131
const prefixedMatch = await db
109132
.selectFrom('cache_keys')
110-
.where('version', '=', opts.version)
133+
.where('version', '=', args.version)
111134
.where('key', 'like', `${key}%`)
112135
.orderBy('cache_keys.updated_at desc')
113136
.selectAll()
@@ -121,7 +144,18 @@ export async function findKeyMatch(opts: { key: string; version: string; restore
121144
}
122145
}
123146

124-
export async function updateOrCreateKey(key: string, version: string, date?: Date) {
147+
export async function updateOrCreateKey(
148+
db: DB,
149+
{
150+
key,
151+
version,
152+
date,
153+
}: {
154+
key: string
155+
version: string
156+
date?: Date
157+
},
158+
) {
125159
const now = date ?? new Date()
126160
const updateResult = await db
127161
.updateTable('cache_keys')
@@ -131,11 +165,14 @@ export async function updateOrCreateKey(key: string, version: string, date?: Dat
131165
.where('version', '=', version)
132166
.executeTakeFirst()
133167
if (Number(updateResult.numUpdatedRows) === 0) {
134-
await createKey(key, version, date)
168+
await createKey(db, { key, version, date })
135169
}
136170
}
137171

138-
export async function touchKey(key: string, version: string, date?: Date) {
172+
export async function touchKey(
173+
db: DB,
174+
{ key, version, date }: { key: string; version: string; date?: Date },
175+
) {
139176
const now = date ?? new Date()
140177
await db
141178
.updateTable('cache_keys')
@@ -145,7 +182,10 @@ export async function touchKey(key: string, version: string, date?: Date) {
145182
.execute()
146183
}
147184

148-
export async function findStaleKeys(olderThanDays: number | undefined, date?: Date) {
185+
export async function findStaleKeys(
186+
db: DB,
187+
{ olderThanDays, date }: { olderThanDays?: number; date?: Date },
188+
) {
149189
if (olderThanDays === undefined) return db.selectFrom('cache_keys').selectAll().execute()
150190

151191
const now = date ?? new Date()
@@ -157,7 +197,10 @@ export async function findStaleKeys(olderThanDays: number | undefined, date?: Da
157197
.execute()
158198
}
159199

160-
export async function createKey(key: string, version: string, date?: Date) {
200+
export async function createKey(
201+
db: DB,
202+
{ key, version, date }: { key: string; version: string; date?: Date },
203+
) {
161204
const now = date ?? new Date()
162205
await db
163206
.insertInto('cache_keys')
@@ -170,10 +213,10 @@ export async function createKey(key: string, version: string, date?: Date) {
170213
.execute()
171214
}
172215

173-
export async function pruneKeys(keys?: Selectable<CacheKeysTable>[]) {
216+
export async function pruneKeys(db: DB, keys?: Selectable<CacheKeysTable>[]) {
174217
if (keys) {
175218
await db.transaction().execute(async (tx) => {
176-
for (const { key, version } of keys) {
219+
for (const { key, version } of keys ?? []) {
177220
await tx
178221
.deleteFrom('cache_keys')
179222
.where('key', '=', key)
@@ -185,3 +228,13 @@ export async function pruneKeys(keys?: Selectable<CacheKeysTable>[]) {
185228
await db.deleteFrom('cache_keys').execute()
186229
}
187230
}
231+
232+
export async function uploadExists(db: DB, { key, version }: { key: string; version: string }) {
233+
const row = await db
234+
.selectFrom('uploads')
235+
.select('id')
236+
.where('key', '=', key)
237+
.where('version', '=', version)
238+
.executeTakeFirst()
239+
return !!row
240+
}

lib/db/migrations.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type Migration, sql } from 'kysely'
22

3-
import type { DatabaseDriverName } from '~/db-drivers'
3+
import type { DatabaseDriverName } from '~/lib/db/drivers'
44

55
export function migrations(dbType: DatabaseDriverName) {
66
return {
@@ -24,5 +24,38 @@ export function migrations(dbType: DatabaseDriverName) {
2424
await db.schema.dropTable('cache_keys').ifExists().execute()
2525
},
2626
},
27+
'2024-10-15T16:39:29': {
28+
async up(db) {
29+
await db.schema
30+
.createTable('uploads')
31+
.addColumn('created_at', 'text', (col) => col.notNull())
32+
.addColumn('key', 'text', (col) => col.notNull())
33+
.addColumn('version', 'text', (col) => col.notNull())
34+
.addColumn('driver_upload_id', 'text', (col) => col.notNull())
35+
.addColumn('id', 'integer', (col) => col.notNull())
36+
.addPrimaryKeyConstraint('pk', ['id'])
37+
.addUniqueConstraint('key_version', ['key', 'version'])
38+
.ifNotExists()
39+
.execute()
40+
await db.schema
41+
.createTable('upload_parts')
42+
.addColumn('upload_id', 'integer')
43+
.addColumn('part_number', 'integer')
44+
.addColumn('e_tag', 'text')
45+
.addPrimaryKeyConstraint('pk', ['upload_id', 'part_number'])
46+
.addForeignKeyConstraint(
47+
'fk_upload_parts_uploads',
48+
['upload_id'],
49+
'uploads',
50+
['id'],
51+
(c) => c.onDelete('cascade'),
52+
)
53+
.ifNotExists()
54+
.execute()
55+
},
56+
async down(db) {
57+
await db.schema.dropTable('uploads').ifExists().execute()
58+
},
59+
},
2760
} satisfies Record<string, Migration>
2861
}

0 commit comments

Comments
 (0)