file_keys: fill in the background, use it once complete
Copying all existing keys in the migration read the files rows from the HDD for over half an hour and held up the deploy. gc:scheduler now copies them in slices (FILE_KEYS_FILL_SECONDS per sweep) and FileStore checks every partition until the fill is complete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
693e6c3244
commit
f0ff8e200f
8 files changed
+163
-18
No files matched your search
@@ -4,20 +4,28 @@ import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||
* A 64-bit hash of every file_key, in one index (LOG.md 2026-10-06 (7)).
|
||||
* file_key is unique per files partition only (D39), so FileStore.insert
|
||||
* looked every key up in every partition: 7 partitions and growing, 35,000
|
||||
* random index reads for a batch of 5,000 files. Now it asks file_keys
|
||||
* first, and only keys whose hash is there (a duplicate, or rarely a
|
||||
* collision) are looked up in files.
|
||||
* random index reads for a batch of 5,000 files. With file_keys complete it
|
||||
* asks file_keys first, and only keys whose hash is there (a duplicate, or
|
||||
* rarely a collision) are looked up in files.
|
||||
*
|
||||
* Filled by the files insert statement trigger; no transaction, so that the
|
||||
* trigger is live before the existing keys are copied and no file inserted
|
||||
* meanwhile is missed (a missed key would let a duplicate file in).
|
||||
* New keys come from the files insert statement trigger. The keys from
|
||||
* before are copied in the background (FileKeys.fill, gc:scheduler):
|
||||
* copying them here took over half an hour of HDD reads and held up the
|
||||
* deploy. Until file_keys_fill.complete, FileStore checks every partition.
|
||||
*/
|
||||
export default class extends BaseSchema {
|
||||
static disableTransactions = true
|
||||
|
||||
async up() {
|
||||
const q = (sql: string) => this.db.rawQuery(sql)
|
||||
await q(`CREATE TABLE IF NOT EXISTS file_keys (key_hash bigint PRIMARY KEY)`)
|
||||
// One row: the fill's position in files.id, the id it must reach, and whether it got there.
|
||||
await q(`
|
||||
CREATE TABLE IF NOT EXISTS file_keys_fill (
|
||||
id smallint PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
||||
next_id bigint NOT NULL DEFAULT 0,
|
||||
until_id bigint,
|
||||
complete boolean NOT NULL DEFAULT false,
|
||||
updated_at timestamptz NOT NULL DEFAULT now())`)
|
||||
await q(`INSERT INTO file_keys_fill (id) VALUES (1) ON CONFLICT DO NOTHING`)
|
||||
await q(`
|
||||
CREATE OR REPLACE FUNCTION files_release_insert_statement() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
@@ -52,17 +60,10 @@ export default class extends BaseSchema {
|
||||
ON CONFLICT DO NOTHING;
|
||||
RETURN NULL;
|
||||
END $$`)
|
||||
// The keys from before; read from the file_key indexes (NVMe), not the files heap (HDD).
|
||||
await q(`SET enable_seqscan = off`)
|
||||
await q(`
|
||||
INSERT INTO file_keys (key_hash)
|
||||
SELECT DISTINCT hashtextextended(file_key, 0) FROM files WHERE file_key IS NOT NULL
|
||||
ON CONFLICT DO NOTHING`)
|
||||
await q(`RESET enable_seqscan`)
|
||||
await q(`ANALYZE file_keys`)
|
||||
}
|
||||
|
||||
async down() {
|
||||
await this.db.rawQuery(`DROP TABLE IF EXISTS file_keys_fill`)
|
||||
await this.db.rawQuery(`DROP TABLE IF EXISTS file_keys`)
|
||||
}
|
||||
}
|
||||
@@ -181,6 +181,21 @@ export class FileKeySchema extends BaseModel {
|
||||
declare keyHash: bigint | number
|
||||
}
|
||||
|
||||
export class FileKeysFillSchema extends BaseModel {
|
||||
static $columns = ['complete', 'id', 'nextId', 'untilId', 'updatedAt'] as const
|
||||
$columns = FileKeysFillSchema.$columns
|
||||
@column()
|
||||
declare complete: boolean
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
@column()
|
||||
declare nextId: bigint | number
|
||||
@column()
|
||||
declare untilId: bigint | number | null
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
declare updatedAt: DateTime
|
||||
}
|
||||
|
||||
export class FileSchema extends BaseModel {
|
||||
static $columns = ['archive', 'contentType', 'createdAt', 'date', 'fileKey', 'filename', 'firstMessageId', 'groups', 'id', 'parts', 'poster', 'realName', 'releaseId', 'segments', 'size', 'updatedAt'] as const
|
||||
$columns = FileSchema.$columns
|
||||
|
||||
Reference in new issue
Block a user