Seed batches: insert files and binaries in chunks of 2,000 rows
A batch of a dense range (over ~5,400 files) exceeded Postgres' 65,535 bind parameters per statement, so its post failed with a 500 until the range was given up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f4ded062df
commit
953bbe9e39
3 files changed
+31
-7
No files matched your search
@@ -36,6 +36,9 @@ export function loadedSegments(stored: Buffer | null): Segment[] {
|
|||||||
return stored ? unpackSegments(stored) : []
|
return stored ? unpackSegments(stored) : []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Rows per INSERT, well below Postgres' 65,535 bind parameters. */
|
||||||
|
export const INSERT_ROWS = 2000
|
||||||
|
|
||||||
class FileStore {
|
class FileStore {
|
||||||
/**
|
/**
|
||||||
* Inserts files, skipping those whose `file_key` exists. Returns the rows
|
* Inserts files, skipping those whose `file_key` exists. Returns the rows
|
||||||
@@ -66,8 +69,13 @@ class FileStore {
|
|||||||
return { ...file, segments: packSegments(sorted), first_message_id: sorted[0]?.id ?? null }
|
return { ...file, segments: packSegments(sorted), first_message_id: sorted[0]?.id ?? null }
|
||||||
})
|
})
|
||||||
if (rows.length === 0) return []
|
if (rows.length === 0) return []
|
||||||
const inserted = await client.knexQuery().table('files').insert(rows).onConflict().ignore().returning(['id', 'filename'])
|
// A statement takes at most 65,535 parameters: ~5,400 rows of 12 columns.
|
||||||
return inserted.map((row: any) => ({ id: Number(row.id), filename: row.filename }))
|
const inserted: { id: number; filename: string }[] = []
|
||||||
|
for (let i = 0; i < rows.length; i += INSERT_ROWS) {
|
||||||
|
const chunk = await client.knexQuery().table('files').insert(rows.slice(i, i + INSERT_ROWS)).onConflict().ignore().returning(['id', 'filename'])
|
||||||
|
for (const row of chunk) inserted.push({ id: Number(row.id), filename: row.filename })
|
||||||
|
}
|
||||||
|
return inserted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import db from '@adonisjs/lucid/services/db'
|
import db from '@adonisjs/lucid/services/db'
|
||||||
import IngestService from '#services/IngestService'
|
import IngestService from '#services/IngestService'
|
||||||
import FileStore from '#services/FileStore'
|
import FileStore, { INSERT_ROWS } from '#services/FileStore'
|
||||||
import SfvService from '#services/SfvService'
|
import SfvService from '#services/SfvService'
|
||||||
import ContentService from '#services/ContentService'
|
import ContentService from '#services/ContentService'
|
||||||
import QueueService from '#services/QueueService'
|
import QueueService from '#services/QueueService'
|
||||||
@@ -78,10 +78,12 @@ class SeedService {
|
|||||||
file_id: fileIds.get(file.fileKey) ?? null,
|
file_id: fileIds.get(file.fileKey) ?? null,
|
||||||
}))
|
}))
|
||||||
binaries.sort((a, b) => a.file_key.localeCompare(b.file_key))
|
binaries.sort((a, b) => a.file_key.localeCompare(b.file_key))
|
||||||
await knex('binaries')
|
for (let i = 0; i < binaries.length; i += INSERT_ROWS) {
|
||||||
.insert(binaries)
|
await knex('binaries')
|
||||||
.onConflict('file_key')
|
.insert(binaries.slice(i, i + INSERT_ROWS))
|
||||||
.merge({ file_id: knex.raw('coalesce(binaries.file_id, excluded.file_id)') as any })
|
.onConflict('file_key')
|
||||||
|
.merge({ file_id: knex.raw('coalesce(binaries.file_id, excluded.file_id)') as any })
|
||||||
|
}
|
||||||
|
|
||||||
// As worker:file does for a new file: an SFV posted earlier may list it (D34), then inspection.
|
// As worker:file does for a new file: an SFV posted earlier may list it (D34), then inspection.
|
||||||
const rowById = new Map(rows.map((row) => [fileIds.get(row.file_key), row]))
|
const rowById = new Map(rows.map((row) => [fileIds.get(row.file_key), row]))
|
||||||
|
|||||||
@@ -46,6 +46,20 @@ test.group('SeedService', (group) => {
|
|||||||
assert.include(again, { files: 0, existing: 2 })
|
assert.include(again, { files: 0, existing: 2 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('stores batches of more files than one INSERT can take', async ({ assert }) => {
|
||||||
|
// 12 columns per row: over 5,461 rows exceed Postgres' 65,535 bind parameters.
|
||||||
|
const aggregator = new FileAggregator<IngestSegment>()
|
||||||
|
const segments = Array.from({ length: 6000 }, (_, i) =>
|
||||||
|
segmentFromHeader(header(`Dense.Upload - [${i + 1}/6000] - "dense.${i + 1}.dat" yEnc (1/1)`, i + 1), 'alt.binaries.test', i + 1)!
|
||||||
|
)
|
||||||
|
const files = aggregator.add(segments).map(toSeedFile)
|
||||||
|
assert.lengthOf(files, 6000)
|
||||||
|
const result = await SeedService.store(files)
|
||||||
|
assert.include(result, { files: 6000 })
|
||||||
|
const [{ count }] = await db.from('binaries').whereLike('name', 'dense.%').whereNotNull('file_id').count('* as count')
|
||||||
|
assert.equal(Number(count), 6000)
|
||||||
|
}).timeout(60_000)
|
||||||
|
|
||||||
test('fragments are kept near the range edges only', async ({ assert }) => {
|
test('fragments are kept near the range edges only', async ({ assert }) => {
|
||||||
const subject = (name: string) => `Seed.Frag - "${name}" yEnc (1/3)`
|
const subject = (name: string) => `Seed.Frag - "${name}" yEnc (1/3)`
|
||||||
const edge = segmentFromHeader(header(subject('edge.bin'), 1), 'alt.binaries.test', 1_000_100)!
|
const edge = segmentFromHeader(header(subject('edge.bin'), 1), 'alt.binaries.test', 1_000_100)!
|
||||||
|
|||||||
Reference in new issue
Block a user