- New src/utils/osLimits.ts: reads RLIMIT_NOFILE (soft/hard, from /proc/self/limits) and vm.max_map_count at open — once per process, Linux-only, measurement-only — and warns loudly when either sits below the pool-scale floors (soft NOFILE < 65536, max_map_count < 262144), with the exact raise commands. On stock defaults the failure otherwise arrives as EMFILE or a failed mmap deep inside an index open, long after the cause stopped being visible. An unreadable limit produces NO warning — no measurement, no claim — so non-Linux platforms stay silent. - Exported for ops doors: checkOsLimits() returns the full OsLimitsReport; floors exported as constants. Wired fire-and-forget in performInit after storage init; the check can never affect open. - Unit tests pin the parser (incl. 'unlimited'), the floor thresholds, the null-never-warns rule, and the off-Linux silent path.
76 lines
3 KiB
TypeScript
76 lines
3 KiB
TypeScript
/**
|
|
* @module tests/unit/utils/osLimits
|
|
* @description OS-limit detection for pool-scale use. Laws:
|
|
* (1) the /proc/self/limits parser reads soft/hard NOFILE exactly, including
|
|
* 'unlimited'; (2) assessment warns ONLY below the pool floors and NEVER
|
|
* on an unreadable (null) limit — no measurement, no claim; (3) the full
|
|
* check composes both sources and survives unreadable /proc silently.
|
|
*/
|
|
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
parseProcLimits,
|
|
assessOsLimits,
|
|
checkOsLimits,
|
|
NOFILE_POOL_FLOOR,
|
|
MAX_MAP_COUNT_POOL_FLOOR
|
|
} from '../../../src/utils/osLimits.js'
|
|
|
|
const SAMPLE_LIMITS = [
|
|
'Limit Soft Limit Hard Limit Units',
|
|
'Max cpu time unlimited unlimited seconds',
|
|
'Max open files 1024 1048576 files',
|
|
'Max locked memory 8388608 8388608 bytes'
|
|
].join('\n')
|
|
|
|
describe('osLimits — detect + warn at pool scale', () => {
|
|
it('parses soft/hard NOFILE from /proc/self/limits, including unlimited', () => {
|
|
expect(parseProcLimits(SAMPLE_LIMITS)).toEqual({ soft: 1024, hard: 1048576 })
|
|
expect(
|
|
parseProcLimits('Max open files unlimited unlimited files')
|
|
).toEqual({ soft: Infinity, hard: Infinity })
|
|
expect(parseProcLimits('no such row here')).toEqual({ soft: null, hard: null })
|
|
})
|
|
|
|
it('warns below the floors, stays quiet at or above them', () => {
|
|
const low = assessOsLimits({ nofileSoft: 1024, nofileHard: 1048576, maxMapCount: 65530 })
|
|
expect(low).toHaveLength(2)
|
|
expect(low[0]).toContain('RLIMIT_NOFILE soft limit is 1024')
|
|
expect(low[0]).toContain(`ulimit -n ${NOFILE_POOL_FLOOR}`)
|
|
expect(low[0]).toContain('raise the soft limit only') // hard already allows it
|
|
expect(low[1]).toContain('vm.max_map_count is 65530')
|
|
expect(low[1]).toContain(`vm.max_map_count=${MAX_MAP_COUNT_POOL_FLOOR}`)
|
|
|
|
expect(
|
|
assessOsLimits({
|
|
nofileSoft: NOFILE_POOL_FLOOR,
|
|
nofileHard: Infinity,
|
|
maxMapCount: MAX_MAP_COUNT_POOL_FLOOR
|
|
})
|
|
).toEqual([])
|
|
})
|
|
|
|
it('an unreadable limit makes NO claim — nulls never warn', () => {
|
|
expect(assessOsLimits({ nofileSoft: null, nofileHard: null, maxMapCount: null })).toEqual([])
|
|
})
|
|
|
|
it('checkOsLimits composes both sources and survives unreadable /proc silently', async () => {
|
|
const report = await checkOsLimits(async (p) => {
|
|
if (p === '/proc/self/limits') return SAMPLE_LIMITS
|
|
if (p === '/proc/sys/vm/max_map_count') return '65530\n'
|
|
throw new Error('unexpected path')
|
|
})
|
|
expect(report.nofileSoft).toBe(1024)
|
|
expect(report.maxMapCount).toBe(65530)
|
|
expect(report.warnings).toHaveLength(2)
|
|
|
|
const offLinux = await checkOsLimits(async () => {
|
|
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
|
|
})
|
|
expect(offLinux).toEqual({
|
|
nofileSoft: null,
|
|
nofileHard: null,
|
|
maxMapCount: null,
|
|
warnings: []
|
|
})
|
|
})
|
|
})
|