redis-smq-common
Version:
RedisSMQ Common Library provides many components that are mainly used by RedisSMQ and RedisSMQ Monitor.
128 lines • 4.81 kB
JavaScript
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