Graceful shutdown JavaScript Executor Microservice and minor change in logs

This commit is contained in:
Vladyslav_Prykhodko 2022-07-07 17:15:59 +03:00
parent addba55cf8
commit bbc43c2572
8 changed files with 122 additions and 91 deletions

View File

@ -16,12 +16,15 @@
import express from 'express'; import express from 'express';
import { _logger} from '../config/logger'; import { _logger} from '../config/logger';
import http from 'http';
import { Socket } from 'net';
export class HttpServer { export class HttpServer {
private logger = _logger('httpServer'); private logger = _logger('httpServer');
private app = express(); private app = express();
private server; private server: http.Server | null;
private connections: Socket[] = [];
constructor(httpPort: number) { constructor(httpPort: number) {
this.app.get('/livenessProbe', async (req, res) => { this.app.get('/livenessProbe', async (req, res) => {
@ -32,15 +35,31 @@ export class HttpServer {
}) })
this.server = this.app.listen(httpPort, () => { this.server = this.app.listen(httpPort, () => {
this.logger.info('Started http endpoint on port %s. Please, use /livenessProbe !', httpPort); this.logger.info('Started HTTP endpoint on port %s. Please, use /livenessProbe !', httpPort);
}).on('error', (error) => { }).on('error', (error) => {
this.logger.error(error); this.logger.error(error);
}); });
}
stop() { this.server.on('connection', connection => {
this.server.close(() => { this.connections.push(connection);
this.logger.info('Http server stop'); connection.on('close', () => this.connections = this.connections.filter(curr => curr !== connection));
}); });
} }
async stop() {
if (this.server) {
this.logger.info('Stopping HTTP Server...');
const _server = this.server;
this.server = null;
this.connections.forEach(curr => curr.end(() => curr.destroy()));
await new Promise<void>(
(resolve, reject) => {
_server.close((err) => {
this.logger.info('HTTP Server stopped.');
resolve();
});
}
);
}
}
} }

View File

@ -59,8 +59,6 @@ export class AwsSqsTemplate implements IQueue {
async init() { async init() {
try { try {
this.logger.info('Starting ThingsBoard JavaScript Executor Microservice...');
this.sqsClient = new SQSClient({ this.sqsClient = new SQSClient({
apiVersion: '2012-11-05', apiVersion: '2012-11-05',
credentials: { credentials: {
@ -129,7 +127,7 @@ export class AwsSqsTemplate implements IQueue {
} catch (e: any) { } catch (e: any) {
this.logger.error('Failed to start ThingsBoard JavaScript Executor Microservice: %s', e.message); this.logger.error('Failed to start ThingsBoard JavaScript Executor Microservice: %s', e.message);
this.logger.error(e.stack); this.logger.error(e.stack);
await this.exit(-1); await this.destroy(-1);
} }
} }
@ -193,23 +191,23 @@ export class AwsSqsTemplate implements IQueue {
return queue; return queue;
} }
async exit(status: number) { async destroy(status: number): Promise<void> {
this.stopped = true; this.stopped = true;
this.logger.info('Exiting with status: %d ...', status); this.logger.info('Exiting with status: %d ...', status);
this.logger.info('Stopping AWS SQS resources...');
if (this.sqsClient) { if (this.sqsClient) {
this.logger.info('Stopping Aws Sqs client.') this.logger.info('Stopping AWS SQS client...');
try { try {
this.sqsClient.destroy(); const _sqsClient = this.sqsClient;
// @ts-ignore // @ts-ignore
delete this.sqsClient; delete this.sqsClient;
this.logger.info('Aws Sqs client stopped.') _sqsClient.destroy();
process.exit(status); this.logger.info('AWS SQS client stopped.');
} catch (e: any) { } catch (e: any) {
this.logger.info('Aws Sqs client stop error.'); this.logger.info('AWS SQS client stop error.');
process.exit(status);
} }
} else {
process.exit(status);
} }
this.logger.info('AWS SQS resources stopped.')
process.exit(status);
} }
} }

View File

@ -56,8 +56,6 @@ export class KafkaTemplate implements IQueue {
async init(): Promise<void> { async init(): Promise<void> {
try { try {
this.logger.info('Starting ThingsBoard JavaScript Executor Microservice...');
const kafkaBootstrapServers: string = config.get('kafka.bootstrap.servers'); const kafkaBootstrapServers: string = config.get('kafka.bootstrap.servers');
const requestTopic: string = config.get('request_topic'); const requestTopic: string = config.get('request_topic');
const useConfluent = config.get('kafka.use_confluent_cloud'); const useConfluent = config.get('kafka.use_confluent_cloud');
@ -119,11 +117,11 @@ export class KafkaTemplate implements IQueue {
const {CRASH} = this.consumer.events; const {CRASH} = this.consumer.events;
this.consumer.on(CRASH, e => { this.consumer.on(CRASH, async (e) => {
this.logger.error(`Got consumer CRASH event, should restart: ${e.payload.restart}`); this.logger.error(`Got consumer CRASH event, should restart: ${e.payload.restart}`);
if (!e.payload.restart) { if (!e.payload.restart) {
this.logger.error('Going to exit due to not retryable error!'); this.logger.error('Going to exit due to not retryable error!');
this.exit(-1); await this.destroy(-1);
} }
}); });
@ -133,7 +131,6 @@ export class KafkaTemplate implements IQueue {
this.sendLoopWithLinger(); this.sendLoopWithLinger();
await this.consumer.subscribe({topic: requestTopic}); await this.consumer.subscribe({topic: requestTopic});
this.logger.info('Started ThingsBoard JavaScript Executor Microservice.');
await this.consumer.run({ await this.consumer.run({
partitionsConsumedConcurrently: this.partitionsConsumedConcurrently, partitionsConsumedConcurrently: this.partitionsConsumedConcurrently,
eachMessage: async ({topic, partition, message}) => { eachMessage: async ({topic, partition, message}) => {
@ -153,7 +150,7 @@ export class KafkaTemplate implements IQueue {
} catch (e: any) { } catch (e: any) {
this.logger.error('Failed to start ThingsBoard JavaScript Executor Microservice: %s', e.message); this.logger.error('Failed to start ThingsBoard JavaScript Executor Microservice: %s', e.message);
this.logger.error(e.stack); this.logger.error(e.stack);
await this.exit(-1); await this.destroy(-1);
} }
} }
@ -242,34 +239,35 @@ export class KafkaTemplate implements IQueue {
} }
async exit(status: number): Promise<void> { async destroy(status: number): Promise<void> {
this.logger.info('Exiting with status: %d ...', status); this.logger.info('Exiting with status: %d ...', status);
this.logger.info('Stopping Kafka resources...');
if (this.kafkaAdmin) { if (this.kafkaAdmin) {
this.logger.info('Stopping Kafka Admin...'); this.logger.info('Stopping Kafka Admin...');
await this.kafkaAdmin.disconnect(); const _kafkaAdmin = this.kafkaAdmin;
// @ts-ignore // @ts-ignore
delete this.kafkaAdmin; delete this.kafkaAdmin;
await _kafkaAdmin.disconnect();
this.logger.info('Kafka Admin stopped.'); this.logger.info('Kafka Admin stopped.');
} }
if (this.consumer) { if (this.consumer) {
this.logger.info('Stopping Kafka Consumer...'); this.logger.info('Stopping Kafka Consumer...');
try { try {
await this.consumer.disconnect(); const _consumer = this.consumer;
// @ts-ignore // @ts-ignore
delete this.consumer; delete this.consumer;
await _consumer.disconnect();
this.logger.info('Kafka Consumer stopped.'); this.logger.info('Kafka Consumer stopped.');
await this.disconnectProducer(); await this.disconnectProducer();
process.exit(status);
} catch (e: any) { } catch (e: any) {
this.logger.info('Kafka Consumer stop error.'); this.logger.info('Kafka Consumer stop error.');
await this.disconnectProducer(); await this.disconnectProducer();
process.exit(status);
} }
} else {
process.exit(status);
} }
this.logger.info('Kafka resources stopped.');
process.exit(status);
} }
private async disconnectProducer(): Promise<void> { private async disconnectProducer(): Promise<void> {
@ -279,9 +277,10 @@ export class KafkaTemplate implements IQueue {
this.logger.info('Stopping loop...'); this.logger.info('Stopping loop...');
clearTimeout(this.sendLoopInstance); clearTimeout(this.sendLoopInstance);
await this.sendMessagesAsBatch(); await this.sendMessagesAsBatch();
await this.producer.disconnect(); const _producer = this.producer;
// @ts-ignore // @ts-ignore
delete this.producer; delete this.producer;
await _producer.disconnect();
this.logger.info('Kafka Producer stopped.'); this.logger.info('Kafka Producer stopped.');
} catch (e) { } catch (e) {
this.logger.info('Kafka Producer stop error.'); this.logger.info('Kafka Producer stop error.');

View File

@ -39,7 +39,6 @@ export class PubSubTemplate implements IQueue {
async init() { async init() {
try { try {
this.logger.info('Starting ThingsBoard JavaScript Executor Microservice...');
this.pubSubClient = new PubSub({ this.pubSubClient = new PubSub({
projectId: this.projectId, projectId: this.projectId,
credentials: this.credentials credentials: this.credentials
@ -82,7 +81,7 @@ export class PubSubTemplate implements IQueue {
} catch (e: any) { } catch (e: any) {
this.logger.error('Failed to start ThingsBoard JavaScript Executor Microservice: %s', e.message); this.logger.error('Failed to start ThingsBoard JavaScript Executor Microservice: %s', e.message);
this.logger.error(e.stack); this.logger.error(e.stack);
await this.exit(-1); await this.destroy(-1);
} }
} }
@ -153,23 +152,23 @@ export class PubSubTemplate implements IQueue {
return queue; return queue;
} }
async exit(status: number): Promise<void> { async destroy(status: number): Promise<void> {
this.logger.info('Exiting with status: %d ...', status); this.logger.info('Exiting with status: %d ...', status);
this.logger.info('Stopping Pub/Sub resources...');
if (this.pubSubClient) { if (this.pubSubClient) {
this.logger.info('Stopping Pub/Sub client.') this.logger.info('Stopping Pub/Sub client...');
try { try {
await this.pubSubClient.close(); const _pubSubClient = this.pubSubClient;
// @ts-ignore // @ts-ignore
delete this.pubSubClient; delete this.pubSubClient;
this.logger.info('Pub/Sub client stopped.') await _pubSubClient.close();
process.exit(status); this.logger.info('Pub/Sub client stopped.');
} catch (e) { } catch (e) {
this.logger.info('Pub/Sub client stop error.'); this.logger.info('Pub/Sub client stop error.');
process.exit(status);
} }
} else {
process.exit(status);
} }
this.logger.info('Pub/Sub resources stopped.');
process.exit(status);
} }
} }

View File

@ -17,5 +17,5 @@
export interface IQueue { export interface IQueue {
init(): Promise<void>; init(): Promise<void>;
send(responseTopic: string, scriptId: string, rawResponse: Buffer, headers: any): Promise<any>; send(responseTopic: string, scriptId: string, rawResponse: Buffer, headers: any): Promise<any>;
exit(status: number): Promise<void>; destroy(status: number): Promise<void>;
} }

View File

@ -49,8 +49,6 @@ export class RabbitMqTemplate implements IQueue {
async init(): Promise<void> { async init(): Promise<void> {
try { try {
this.logger.info('Starting ThingsBoard JavaScript Executor Microservice...');
const url = `amqp://${this.username}:${this.password}@${this.host}:${this.port}${this.vhost}`; const url = `amqp://${this.username}:${this.password}@${this.host}:${this.port}${this.vhost}`;
this.connection = await amqp.connect(url); this.connection = await amqp.connect(url);
this.channel = await this.connection.createConfirmChannel(); this.channel = await this.connection.createConfirmChannel();
@ -78,7 +76,7 @@ export class RabbitMqTemplate implements IQueue {
} catch (e: any) { } catch (e: any) {
this.logger.error('Failed to start ThingsBoard JavaScript Executor Microservice: %s', e.message); this.logger.error('Failed to start ThingsBoard JavaScript Executor Microservice: %s', e.message);
this.logger.error(e.stack); this.logger.error(e.stack);
await this.exit(-1); await this.destroy(-1);
} }
} }
@ -120,32 +118,33 @@ export class RabbitMqTemplate implements IQueue {
return queue; return queue;
} }
async exit(status: number) { async destroy(status: number) {
this.logger.info('Exiting with status: %d ...', status); this.logger.info('Exiting with status: %d ...', status);
this.logger.info('Stopping RabbitMQ resources...');
if (this.channel) { if (this.channel) {
this.logger.info('Stopping RabbitMq chanel.') this.logger.info('Stopping RabbitMQ chanel...');
await this.channel.close(); const _channel = this.channel;
// @ts-ignore // @ts-ignore
delete this.channel; delete this.channel;
this.logger.info('RabbitMq chanel stopped'); await _channel.close();
this.logger.info('RabbitMQ chanel stopped');
} }
if (this.connection) { if (this.connection) {
this.logger.info('Stopping RabbitMq connection.') this.logger.info('Stopping RabbitMQ connection...')
try { try {
await this.connection.close(); const _connection = this.connection;
// @ts-ignore // @ts-ignore
delete this.connection; delete this.connection;
this.logger.info('RabbitMq client connection.') await _connection.close();
process.exit(status); this.logger.info('RabbitMQ client connection.');
} catch (e) { } catch (e) {
this.logger.info('RabbitMq connection stop error.'); this.logger.info('RabbitMQ connection stop error.');
process.exit(status);
} }
} else {
process.exit(status);
} }
this.logger.info('RabbitMQ resources stopped.')
process.exit(status);
} }
} }

View File

@ -49,8 +49,6 @@ export class ServiceBusTemplate implements IQueue {
async init() { async init() {
try { try {
this.logger.info('Starting ThingsBoard JavaScript Executor Microservice...');
const connectionString = `Endpoint=sb://${this.namespaceName}.servicebus.windows.net/;SharedAccessKeyName=${this.sasKeyName};SharedAccessKey=${this.sasKey}`; const connectionString = `Endpoint=sb://${this.namespaceName}.servicebus.windows.net/;SharedAccessKeyName=${this.sasKeyName};SharedAccessKey=${this.sasKey}`;
this.sbClient = new ServiceBusClient(connectionString) this.sbClient = new ServiceBusClient(connectionString)
this.serviceBusService = new ServiceBusAdministrationClient(connectionString); this.serviceBusService = new ServiceBusAdministrationClient(connectionString);
@ -84,7 +82,7 @@ export class ServiceBusTemplate implements IQueue {
} catch (e: any) { } catch (e: any) {
this.logger.error('Failed to start ThingsBoard JavaScript Executor Microservice: %s', e.message); this.logger.error('Failed to start ThingsBoard JavaScript Executor Microservice: %s', e.message);
this.logger.error(e.stack); this.logger.error(e.stack);
await this.exit(-1); await this.destroy(-1);
} }
} }
@ -141,32 +139,45 @@ export class ServiceBusTemplate implements IQueue {
return queue; return queue;
} }
async exit(status: number) { async destroy(status: number) {
this.logger.info('Exiting with status: %d ...', status); this.logger.info('Exiting with status: %d ...', status);
this.logger.info('Stopping Azure Service Bus resources...') this.logger.info('Stopping Azure Service Bus resources...')
if (this.receiver) { if (this.receiver) {
this.logger.info('Stopping Service Bus Receiver...');
try { try {
await this.receiver.close(); const _receiver = this.receiver;
// @ts-ignore // @ts-ignore
delete this.receiver; delete this.receiver;
await _receiver.close();
this.logger.info('Service Bus Receiver stopped.');
} catch (e) { } catch (e) {
this.logger.info('Service Bus Receiver stop error.');
} }
} }
this.senderMap.forEach(k => { this.logger.info('Stopping Service Bus Senders...');
try { const senders: Promise<void>[] = [];
k.close(); this.senderMap.forEach((sender) => {
} catch (e) { senders.push(sender.close());
}
}); });
this.senderMap.clear(); this.senderMap.clear();
try {
await Promise.all(senders);
this.logger.info('Service Bus Senders stopped.');
} catch (e) {
this.logger.info('Service Bus Senders stop error.');
}
if (this.sbClient) { if (this.sbClient) {
this.logger.info('Stopping Service Bus Client...');
try { try {
await this.sbClient.close(); const _sbClient = this.sbClient;
// @ts-ignore // @ts-ignore
delete this.sbClient; delete this.sbClient;
await _sbClient.close();
this.logger.info('Service Bus Client stopped.');
} catch (e) { } catch (e) {
this.logger.info('Service Bus Client stop error.');
} }
} }
this.logger.info('Azure Service Bus resources stopped.') this.logger.info('Azure Service Bus resources stopped.')

View File

@ -32,33 +32,34 @@ logger.info('===CONFIG END===');
const serviceType = config.get('queue_type'); const serviceType = config.get('queue_type');
const httpPort = Number(config.get('http_port')); const httpPort = Number(config.get('http_port'));
let queues: IQueue; let queues: IQueue | null;
let httpServer: HttpServer; let httpServer: HttpServer | null;
(async () => { (async () => {
logger.info('Starting ThingsBoard JavaScript Executor Microservice...');
switch (serviceType) { switch (serviceType) {
case 'kafka': case 'kafka':
logger.info('Starting kafka template.'); logger.info('Starting Kafka template...');
queues = await KafkaTemplate.build(); queues = await KafkaTemplate.build();
logger.info('kafka template started.'); logger.info('Kafka template started.');
break; break;
case 'pubsub': case 'pubsub':
logger.info('Starting Pub/Sub template.') logger.info('Starting Pub/Sub template...')
queues = await PubSubTemplate.build(); queues = await PubSubTemplate.build();
logger.info('Pub/Sub template started.') logger.info('Pub/Sub template started.')
break; break;
case 'aws-sqs': case 'aws-sqs':
logger.info('Starting Aws Sqs template.') logger.info('Starting AWS SQS template...')
queues = await AwsSqsTemplate.build(); queues = await AwsSqsTemplate.build();
logger.info('Aws Sqs template started.') logger.info('AWS SQS template started.')
break; break;
case 'rabbitmq': case 'rabbitmq':
logger.info('Starting RabbitMq template.') logger.info('Starting RabbitMQ template...')
queues = await RabbitMqTemplate.build(); queues = await RabbitMqTemplate.build();
logger.info('RabbitMq template started.') logger.info('RabbitMQ template started.')
break; break;
case 'service-bus': case 'service-bus':
logger.info('Starting Azure Service Bus template.') logger.info('Starting Azure Service Bus template...')
queues = await ServiceBusTemplate.build(); queues = await ServiceBusTemplate.build();
logger.info('Azure Service Bus template started.') logger.info('Azure Service Bus template started.')
break; break;
@ -70,17 +71,22 @@ let httpServer: HttpServer;
httpServer = new HttpServer(httpPort); httpServer = new HttpServer(httpPort);
})(); })();
process.on('SIGTERM', () => { [`SIGINT`, `SIGUSR1`, `SIGUSR2`, `uncaughtException`, `SIGTERM`].forEach((eventType) => {
logger.info('SIGTERM signal received'); process.on(eventType, async () => {
process.exit(0); logger.info(`${eventType} signal received`);
}); if (httpServer) {
const _httpServer = httpServer;
httpServer = null;
await _httpServer.stop();
}
if (queues) {
const _queues = queues;
queues = null;
await _queues.destroy(0);
}
})
})
process.on('exit', async () => { process.on('exit', (code: number) => {
if (httpServer) { logger.info(`JavaScript Executor Microservice has been stopped. Exit code: ${code}.`);
httpServer.stop();
}
if (queues) {
queues.exit(0);
}
logger.info('JavaScript Executor Microservice has been stopped.');
}); });