UNPKG

redis-smq-common

Version:

Provides essential components and utilities shared across RedisSMQ packages.

136 lines 4.58 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, RedisServerStartupFailedError, } from './errors/index.js'; const { REDIS_BINARY_PATH } = constants; const execAsync = promisify(exec); export class RedisServer { process = null; port = null; powerSwitch = new PowerSwitch(); constructor() { this.bindExitHandler(); } bindExitHandler() { process.once('beforeExit', () => this.shutdown()); } async findRedisBinary() { try { const command = process.platform === 'win32' ? 'where redis-server' : 'which redis-server'; const { stdout } = await execAsync(command); const path = stdout.trim(); if (path) return path; } catch { } if (await env.doesPathExist(REDIS_BINARY_PATH)) { return REDIS_BINARY_PATH; } throw new RedisServerBinaryNotFoundError(); } createRedisArgs(port) { return ['--port', port.toString(), '--save', '', '--appendonly', 'no']; } waitForReady(timeoutMs) { return new Promise((resolve, reject) => { if (!this.process) { reject(new Error('Redis process not started')); return; } const timeout = setTimeout(() => { cleanup(); reject(new RedisServerStartupFailedError({ message: `Redis server start timeout after ${timeoutMs}ms`, })); }, timeoutMs); const onData = (data) => { if (data.toString().includes('Ready to accept connections')) { cleanup(); resolve(); } }; const onExit = () => { cleanup(); }; const onError = (error) => { cleanup(); reject(new RedisServerStartupFailedError({ message: `Redis process error: ${error.message}`, })); }; const cleanup = () => { clearTimeout(timeout); this.process?.stdout?.removeListener('data', onData); this.process?.removeListener('exit', onExit); this.process?.removeListener('error', onError); }; this.process.stdout?.on('data', onData); this.process.on('exit', onExit); this.process.on('error', onError); }); } waitForShutdown(timeoutMs) { return new Promise((resolve) => { if (!this.process) { resolve(); return; } const timeout = setTimeout(() => { cleanup(); this.process?.kill('SIGKILL'); resolve(); }, timeoutMs); const onExit = () => { cleanup(); resolve(); }; const cleanup = () => { clearTimeout(timeout); this.process?.removeListener('exit', onExit); }; this.process.once('exit', onExit); this.process.kill('SIGTERM'); }); } async start(requestedPort) { if (!this.powerSwitch.goingUp()) { throw new Error('Already started or going up'); } const redisBinary = await this.findRedisBinary(); this.port = requestedPort || (await net.getRandomPort()); this.process = spawn(redisBinary, this.createRedisArgs(this.port)); try { await this.waitForReady(10000); } catch (error) { if (this.process && !this.process.exitCode && !this.process.killed) { throw error; } return this.port; } this.powerSwitch.commit(); return this.port; } async shutdown() { if (!this.powerSwitch.goingDown()) return; try { if (this.process) { await this.waitForShutdown(5000); } } finally { this.process = null; this.port = null; this.powerSwitch.commit(); } } } //# sourceMappingURL=redis-server.js.map