UNPKG

redis-smq-common

Version:

RedisSMQ Common Library provides many components that are mainly used by RedisSMQ and RedisSMQ Monitor.

128 lines 4.81 kB
import { spawn } from 'child_process'; import { exec } from 'node:child_process'; import { promisify } from 'node:util'; import { env } from '../env/index.js'; import { net } from '../net/index.js'; import { PowerSwitch } from '../power-switch/index.js'; import { constants } from './constants.js'; import { RedisServerBinaryNotFoundError } from './errors/index.js'; const { REDIS_BINARY_PATH } = constants; const execAsync = promisify(exec); export class RedisServer { redisBinaryPath = null; redisChildProcess = null; redisPort = null; powerSwitch = new PowerSwitch(); static STARTUP_TIMEOUT = 10000; static SHUTDOWN_TIMEOUT = 5000; constructor() { this.setupGlobalProcessListeners(); } setupGlobalProcessListeners() { process.on('SIGINT', this.handleProcessExit); process.on('SIGTERM', this.handleProcessExit); } handleProcessExit = async () => { await this.shutdown(); }; async waitForRedisServerStartup() { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { cleanUp(); reject(new Error('Redis server start timeout')); }, RedisServer.STARTUP_TIMEOUT); const onData = (data) => { if (data.toString().includes('Ready to accept connections')) { cleanUp(); resolve(Number(this.redisPort)); } }; const onError = (error) => { cleanUp(); reject(error); }; const onExit = (code) => { cleanUp(); reject(new Error(`Redis server exited with code ${code}`)); }; this.redisChildProcess?.stdout?.on('data', onData); this.redisChildProcess?.on('error', onError); this.redisChildProcess?.on('exit', onExit); const cleanUp = () => { clearTimeout(timeout); this.redisChildProcess?.stdout?.removeListener('data', onData); this.redisChildProcess?.removeListener('error', onError); this.redisChildProcess?.removeListener('exit', onExit); }; }); } setupChildProcessListeners() { this.redisChildProcess?.stderr?.on('data', (data) => { console.error(`Redis stderr (${this.redisPort}): ${data.toString().trim()}`); }); this.redisChildProcess?.on('error', () => this.shutdown()); this.redisChildProcess?.on('exit', () => this.shutdown()); } async fetchSystemWideRedisBinaryPath() { try { const { stdout } = await execAsync('which redis-server || where redis-server'); return stdout.trim() || null; } catch { return null; } } async getRedisServerBinaryPath() { const systemWideBinaryPath = await this.fetchSystemWideRedisBinaryPath(); if (systemWideBinaryPath) return systemWideBinaryPath; if (await env.doesPathExist(REDIS_BINARY_PATH)) { return REDIS_BINARY_PATH; } return null; } async start(port) { const goingUp = this.powerSwitch.goingUp(); if (!goingUp) { throw new Error('Cannot start Redis server while it is already running.'); } this.redisBinaryPath = await this.getRedisServerBinaryPath(); if (!this.redisBinaryPath) { throw new RedisServerBinaryNotFoundError(); } this.redisPort = port ?? (await net.getRandomPort()); this.redisChildProcess = spawn(this.redisBinaryPath, [ '--appendonly', 'no', '--save', '', '--port', this.redisPort.toString(), ]); this.setupChildProcessListeners(); await this.waitForRedisServerStartup(); this.powerSwitch.commit(); return this.redisPort; } async shutdown() { const goingDown = this.powerSwitch.goingDown(); if (!goingDown) return; await new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.redisChildProcess?.kill('SIGKILL'); reject(new Error('Redis server did not shut down gracefully.')); }, RedisServer.SHUTDOWN_TIMEOUT); const cleanUp = () => { clearTimeout(timeout); this.redisChildProcess = null; this.redisPort = null; resolve(); }; this.redisChildProcess?.once('close', cleanUp); this.redisChildProcess?.kill('SIGTERM'); }); this.powerSwitch.commit(); } } //# sourceMappingURL=redis-server.js.map