mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 23:11:44 +00:00
fix(core): gracefully shutdown the app when repl exits
The application module we pass to repl may have long-running phases (e.g. database connection polling) and if we don't finish those long-running phases the repl would stuck on exit possibly forever. I encoutered this issue while using MikroORM.forRoot module. The solution is to call `app.close()` when repl exits. Modules should implement graceful shutdown using lifecycle hooks.
This commit is contained in:
70
integration/repl/e2e/repl-process.spec.ts
Normal file
70
integration/repl/e2e/repl-process.spec.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { expect } from 'chai';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
const PROMPT = '> ';
|
||||
|
||||
describe('REPL process', function () {
|
||||
let replProcess: ReturnType<typeof spawn>;
|
||||
|
||||
function waitForReplToStart(
|
||||
process: ReturnType<typeof spawn>,
|
||||
message,
|
||||
timeout = 10000,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error('REPL did not start in time'));
|
||||
}, timeout);
|
||||
|
||||
if (!process.stdout || !process.stderr) {
|
||||
return reject(new Error('REPL stdout or stderr is not available'));
|
||||
}
|
||||
process.stdout.on('data', data => {
|
||||
if (data.toString().includes(message)) {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
process.stderr.on('data', data => {
|
||||
if (data.toString().includes(message)) {
|
||||
clearTimeout(timer);
|
||||
reject(new Error(`REPL started with error: ${data}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async function () {
|
||||
this.timeout(15000);
|
||||
replProcess = spawn('ts-node', ['../src/repl.ts'], { cwd: __dirname });
|
||||
await waitForReplToStart(replProcess, PROMPT);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (replProcess) {
|
||||
replProcess.kill(9);
|
||||
}
|
||||
});
|
||||
|
||||
it('exits on .exit', async function () {
|
||||
this.timeout(1000);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
replProcess.on('exit', _ => {
|
||||
expect(replProcess.exitCode).to.equal(0);
|
||||
resolve();
|
||||
});
|
||||
|
||||
replProcess.on('error', err => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
if (replProcess.stdin) {
|
||||
replProcess.stdin.write('.exit\n');
|
||||
} else {
|
||||
reject(new Error('REPL stdin is not available'));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
24
integration/repl/src/database/database.connection.ts
Normal file
24
integration/repl/src/database/database.connection.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseConnection implements OnModuleDestroy {
|
||||
keepAlive = true;
|
||||
|
||||
static connect(): DatabaseConnection {
|
||||
const connection = new DatabaseConnection();
|
||||
connection.maintainConnection();
|
||||
return connection;
|
||||
}
|
||||
|
||||
onModuleDestroy() {
|
||||
this.keepAlive = false;
|
||||
}
|
||||
|
||||
maintainConnection() {
|
||||
setTimeout(() => {
|
||||
if (this.keepAlive) {
|
||||
this.maintainConnection();
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
20
integration/repl/src/database/database.module.ts
Normal file
20
integration/repl/src/database/database.module.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { DynamicModule, Module } from '@nestjs/common';
|
||||
import { DatabaseConnection } from './database.connection';
|
||||
|
||||
@Module({})
|
||||
export class DatabaseModule {
|
||||
static forRoot(): DynamicModule {
|
||||
const connectionProvider = {
|
||||
provide: DatabaseConnection,
|
||||
useFactory: () => {
|
||||
return DatabaseConnection.connect();
|
||||
},
|
||||
};
|
||||
return {
|
||||
global: true,
|
||||
module: DatabaseModule,
|
||||
providers: [connectionProvider],
|
||||
exports: [connectionProvider],
|
||||
};
|
||||
}
|
||||
}
|
||||
7
integration/repl/src/long-living-app.module.ts
Normal file
7
integration/repl/src/long-living-app.module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DatabaseModule } from './database/database.module';
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule.forRoot()],
|
||||
})
|
||||
export class LongLivingAppModule {}
|
||||
7
integration/repl/src/repl.ts
Normal file
7
integration/repl/src/repl.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { repl } from '@nestjs/core';
|
||||
import { LongLivingAppModule } from './long-living-app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
await repl(LongLivingAppModule);
|
||||
}
|
||||
bootstrap();
|
||||
@@ -32,5 +32,9 @@ export async function repl(
|
||||
|
||||
defineDefaultCommandsOnRepl(replServer);
|
||||
|
||||
replServer.on('exit', async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
return replServer;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user