/** * @module utils/osLimits * @description Detect-and-warn for OS resource limits that bite at POOL scale. * * A single brain rarely notices them, but a pool of brains — especially with a * native accelerator memory-mapping many index files per brain — consumes file * descriptors and memory mappings multiplicatively. On stock Linux defaults * (RLIMIT_NOFILE soft 1024, vm.max_map_count 65530) the failure arrives as * EMFILE or a failed mmap deep inside an index open, long after the real cause * (the limit) stopped being visible. This module reads the limits at open and * WARNS ONCE per process with the exact raise commands, so the operator learns * the fix before the incident instead of from it. * * Read-only and Linux-only by construction: both sources are `/proc` files. * On platforms where they are absent the check reports nulls and stays silent — * no limit read means no claim made, never a guessed warning. */ import * as fs from 'node:fs' import { prodLog } from './logger.js' /** Soft-NOFILE floor below which pool-scale use is at EMFILE risk. */ export const NOFILE_POOL_FLOOR = 65536 /** vm.max_map_count floor below which mmap-heavy native indexes are at risk. */ export const MAX_MAP_COUNT_POOL_FLOOR = 262144 export interface OsLimitsReport { /** RLIMIT_NOFILE soft limit (null when unreadable; Infinity for 'unlimited'). */ nofileSoft: number | null /** RLIMIT_NOFILE hard limit (null when unreadable; Infinity for 'unlimited'). */ nofileHard: number | null /** vm.max_map_count (null when unreadable). */ maxMapCount: number | null /** Human-actionable warnings for limits below the pool floors. Empty = fine. */ warnings: string[] } /** * Parse the `Max open files` row of a `/proc//limits` document into * soft/hard values. Returns nulls when the row is absent or malformed. */ export function parseProcLimits(content: string): { soft: number | null; hard: number | null } { const line = content.split('\n').find((l) => l.startsWith('Max open files')) if (!line) return { soft: null, hard: null } const m = line.match(/^Max open files\s+(\S+)\s+(\S+)/) if (!m) return { soft: null, hard: null } const parse = (v: string): number | null => { if (v === 'unlimited') return Infinity const n = Number.parseInt(v, 10) return Number.isNaN(n) ? null : n } return { soft: parse(m[1]), hard: parse(m[2]) } } /** * Assess readable limits against the pool floors. Pure — feed it any values. * A null (unreadable) limit produces NO warning: no measurement, no claim. */ export function assessOsLimits(limits: { nofileSoft: number | null nofileHard: number | null maxMapCount: number | null }): string[] { const warnings: string[] = [] if (limits.nofileSoft !== null && limits.nofileSoft < NOFILE_POOL_FLOOR) { const hardNote = limits.nofileHard !== null && limits.nofileHard >= NOFILE_POOL_FLOOR ? ` (the hard limit ${limits.nofileHard === Infinity ? 'unlimited' : limits.nofileHard} already allows it — raise the soft limit only)` : '' warnings.push( `RLIMIT_NOFILE soft limit is ${limits.nofileSoft} — below the ${NOFILE_POOL_FLOOR} recommended ` + `for pool-scale use (a pool of brains with a native accelerator opens many index files per brain; ` + `the failure mode is EMFILE deep inside an index open). Raise with \`ulimit -n ${NOFILE_POOL_FLOOR}\` ` + `or LimitNOFILE=${NOFILE_POOL_FLOOR} in the service unit${hardNote}.` ) } if (limits.maxMapCount !== null && limits.maxMapCount < MAX_MAP_COUNT_POOL_FLOOR) { warnings.push( `vm.max_map_count is ${limits.maxMapCount} — below the ${MAX_MAP_COUNT_POOL_FLOOR} recommended ` + `for mmap-heavy native indexes at pool scale (each mapped index segment consumes map entries; ` + `the failure mode is a failed mmap mid-heal). Raise with ` + `\`sysctl -w vm.max_map_count=${MAX_MAP_COUNT_POOL_FLOOR}\` (persist in /etc/sysctl.d/).` ) } return warnings } /** * Read the limits from /proc and assess them. `readFile` is injectable for * tests; absent/unreadable sources yield nulls (and therefore no warnings). */ export async function checkOsLimits( readFile: (path: string) => Promise = async (p) => fs.promises.readFile(p, 'utf-8') ): Promise { let nofileSoft: number | null = null let nofileHard: number | null = null let maxMapCount: number | null = null try { const parsed = parseProcLimits(await readFile('/proc/self/limits')) nofileSoft = parsed.soft nofileHard = parsed.hard } catch { // Not Linux (or /proc unavailable) — no measurement, no claim. } try { const raw = (await readFile('/proc/sys/vm/max_map_count')).trim() const n = Number.parseInt(raw, 10) maxMapCount = Number.isNaN(n) ? null : n } catch { // Not Linux — same rule. } const warnings = assessOsLimits({ nofileSoft, nofileHard, maxMapCount }) return { nofileSoft, nofileHard, maxMapCount, warnings } } /** Once-per-process latch so a brain pool warns once, not once per brain. */ let osLimitsWarned = false /** * Run the check and warn (once per process) about limits below the pool * floors. Called from brain open; safe everywhere (silent off-Linux). */ export async function warnOnLowOsLimits(): Promise { if (osLimitsWarned) return osLimitsWarned = true try { const report = await checkOsLimits() for (const warning of report.warnings) { prodLog.warn(`[Brainy] OS limit check: ${warning}`) } } catch { // The check must never affect open — measurement-only. } }