Rebuild: stats from a fixed-size sample, refreshed every 5 minutes, given up after 2
TABLESAMPLE SYSTEM (0.5) read 30 s of random pages on the HDD at 7M releases, growing with the catalog; a refresh that never finished kept the header's totals 3.5 hours old (it blocked every later refresh). Now TABLESAMPLE SYSTEM_ROWS (5000) (tsm_system_rows), every 5 minutes, and a refresh is given up after 2 minutes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f642a49d7d
commit
99fffe30f4
2 files changed
+23
-5
No files matched your search
@@ -61,7 +61,9 @@ const SORTS: Record<SortOrder, string> = {
|
||||
|
||||
/** A broad search needn't count every match (each counted row is a heap read). */
|
||||
const COUNT_LIMIT = 10_000
|
||||
const STATS_TTL_MS = 60 * 1000
|
||||
const STATS_TTL_MS = 5 * 60 * 1000
|
||||
/** A refresh that takes longer is given up (the next request tries again). */
|
||||
const STATS_TIMEOUT_MS = 2 * 60 * 1000
|
||||
|
||||
/** ILIKE pattern of one word (the words are split on dots, dashes, underscores and spaces). */
|
||||
function contains(word: string): string {
|
||||
@@ -188,7 +190,10 @@ class SearchService {
|
||||
/** Totals for the web interface, computed at most once a minute (callers in between share them). */
|
||||
async stats() {
|
||||
if (!this.statsCache || Date.now() - this.statsCache.at > STATS_TTL_MS) {
|
||||
this.statsRefresh ??= this.computeStats()
|
||||
this.statsRefresh ??= Promise.race([
|
||||
this.computeStats(),
|
||||
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timed out')), STATS_TIMEOUT_MS).unref()),
|
||||
])
|
||||
.then((value) => {
|
||||
this.statsCache = { at: Date.now(), value }
|
||||
})
|
||||
@@ -210,13 +215,14 @@ class SearchService {
|
||||
/**
|
||||
* Estimates (a catalog of 100M+ rows can't be counted on every visit):
|
||||
* the row count from Postgres's statistics, below a million rows exact;
|
||||
* readable share, files and size from a sample; ranges counted exactly
|
||||
* (a few hundred thousand rows).
|
||||
* readable share, files and size from a sample of 5,000 rows; ranges
|
||||
* counted exactly (a few hundred thousand rows).
|
||||
*/
|
||||
private async computeStats() {
|
||||
const { rows } = await db.rawQuery(`SELECT reltuples::bigint AS n FROM pg_class WHERE relname = 'releases'`)
|
||||
const estimate = Number(rows[0]?.n ?? 0)
|
||||
const sample = estimate < 1_000_000 ? '' : 'TABLESAMPLE SYSTEM (0.5)'
|
||||
// A fixed-size sample: a percentage of a big catalog is many random reads on the HDD.
|
||||
const sample = estimate < 1_000_000 ? '' : 'TABLESAMPLE SYSTEM_ROWS (5000)'
|
||||
const { rows: s } = await db.rawQuery(
|
||||
`SELECT count(*) AS sampled, count(*) FILTER (WHERE readable) AS readable, coalesce(sum(files), 0) AS files, coalesce(sum(bytes), 0) AS bytes
|
||||
FROM releases ${sample} WHERE files > 0`
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||
|
||||
/** TABLESAMPLE SYSTEM_ROWS: the stats' sample of the catalog at a fixed size (a percentage read 30 s of random pages on the HDD at 7M releases). */
|
||||
export default class extends BaseSchema {
|
||||
async up() {
|
||||
this.schema.raw(`CREATE EXTENSION IF NOT EXISTS tsm_system_rows`)
|
||||
}
|
||||
|
||||
async down() {
|
||||
this.schema.raw(`DROP EXTENSION IF EXISTS tsm_system_rows`)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user