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). */
|
/** A broad search needn't count every match (each counted row is a heap read). */
|
||||||
const COUNT_LIMIT = 10_000
|
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). */
|
/** ILIKE pattern of one word (the words are split on dots, dashes, underscores and spaces). */
|
||||||
function contains(word: string): string {
|
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). */
|
/** Totals for the web interface, computed at most once a minute (callers in between share them). */
|
||||||
async stats() {
|
async stats() {
|
||||||
if (!this.statsCache || Date.now() - this.statsCache.at > STATS_TTL_MS) {
|
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) => {
|
.then((value) => {
|
||||||
this.statsCache = { at: Date.now(), 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):
|
* 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;
|
* the row count from Postgres's statistics, below a million rows exact;
|
||||||
* readable share, files and size from a sample; ranges counted exactly
|
* readable share, files and size from a sample of 5,000 rows; ranges
|
||||||
* (a few hundred thousand rows).
|
* counted exactly (a few hundred thousand rows).
|
||||||
*/
|
*/
|
||||||
private async computeStats() {
|
private async computeStats() {
|
||||||
const { rows } = await db.rawQuery(`SELECT reltuples::bigint AS n FROM pg_class WHERE relname = 'releases'`)
|
const { rows } = await db.rawQuery(`SELECT reltuples::bigint AS n FROM pg_class WHERE relname = 'releases'`)
|
||||||
const estimate = Number(rows[0]?.n ?? 0)
|
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(
|
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
|
`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`
|
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