@@ -26,6 +26,30 @@ import {LifeCycleObserverRegistry} from './lifecycle-registry';
2626import { Server } from './server' ;
2727import { createServiceBinding , ServiceOptions } from './service' ;
2828const debug = debugFactory ( 'loopback:core:application' ) ;
29+ const debugShutdown = debugFactory ( 'loopback:core:application:shutdown' ) ;
30+ const debugWarning = debugFactory ( 'loopback:core:application:warning' ) ;
31+
32+ /**
33+ * A helper function to build constructor args for `Context`
34+ * @param configOrParent - Application config or parent context
35+ * @param parent - Parent context if the first arg is application config
36+ */
37+ function buildConstructorArgs (
38+ configOrParent ?: ApplicationConfig | Context ,
39+ parent ?: Context ,
40+ ) {
41+ let name : string | undefined ;
42+ let parentCtx : Context | undefined ;
43+
44+ if ( configOrParent instanceof Context ) {
45+ parentCtx = configOrParent ;
46+ name = undefined ;
47+ } else {
48+ parentCtx = parent ;
49+ name = configOrParent ?. name ;
50+ }
51+ return [ parentCtx , name ] ;
52+ }
2953
3054/**
3155 * Application is the container for various types of artifacts, such as
@@ -39,6 +63,8 @@ export class Application extends Context implements LifeCycleObserver {
3963 * A flag to indicate that the application is being shut down
4064 */
4165 private _isShuttingDown = false ;
66+ private _shutdownOptions : ShutdownOptions ;
67+ private _signalListener : ( signal : string ) => Promise < void > ;
4268
4369 /**
4470 * State of the application
@@ -81,13 +107,14 @@ export class Application extends Context implements LifeCycleObserver {
81107 constructor ( config ?: ApplicationConfig , parent ?: Context ) ;
82108
83109 constructor ( configOrParent ?: ApplicationConfig | Context , parent ?: Context ) {
84- super (
85- configOrParent instanceof Context ? configOrParent : parent ,
86- 'application' ,
87- ) ;
110+ // super() has to be first statement for a constructor
111+ super ( ...buildConstructorArgs ( configOrParent , parent ) ) ;
112+
113+ this . options =
114+ configOrParent instanceof Context ? { } : configOrParent ?? { } ;
88115
89- if ( configOrParent instanceof Context ) configOrParent = { } ;
90- this . options = configOrParent ?? { } ;
116+ // Configure debug
117+ this . _debug = debug ;
91118
92119 // Bind the life cycle observer registry
93120 this . bind ( CoreBindings . LIFE_CYCLE_OBSERVER_REGISTRY )
@@ -98,11 +125,7 @@ export class Application extends Context implements LifeCycleObserver {
98125 // Make options available to other modules as well.
99126 this . bind ( CoreBindings . APPLICATION_CONFIG ) . to ( this . options ) ;
100127
101- const shutdownConfig = this . options . shutdown ?? { } ;
102- this . setupShutdown (
103- shutdownConfig . signals ?? [ 'SIGTERM' ] ,
104- shutdownConfig . gracePeriod ,
105- ) ;
128+ this . _shutdownOptions = { signals : [ 'SIGTERM' ] , ...this . options . shutdown } ;
106129 }
107130
108131 /**
@@ -123,7 +146,7 @@ export class Application extends Context implements LifeCycleObserver {
123146 * ```
124147 */
125148 controller ( controllerCtor : ControllerClass , name ?: string ) : Binding {
126- debug ( 'Adding controller %s' , name ?? controllerCtor . name ) ;
149+ this . debug ( 'Adding controller %s' , name ?? controllerCtor . name ) ;
127150 const binding = createBindingFromClass ( controllerCtor , {
128151 name,
129152 namespace : CoreBindings . CONTROLLERS ,
@@ -156,7 +179,7 @@ export class Application extends Context implements LifeCycleObserver {
156179 ctor : Constructor < T > ,
157180 name ?: string ,
158181 ) : Binding < T > {
159- debug ( 'Adding server %s' , name ?? ctor . name ) ;
182+ this . debug ( 'Adding server %s' , name ?? ctor . name ) ;
160183 const binding = createBindingFromClass ( ctor , {
161184 name,
162185 namespace : CoreBindings . SERVERS ,
@@ -270,6 +293,8 @@ export class Application extends Context implements LifeCycleObserver {
270293 // No-op if it's started
271294 if ( this . _state === 'started' ) return ;
272295 this . setState ( 'starting' ) ;
296+ this . setupShutdown ( ) ;
297+
273298 const registry = await this . getLifeCycleObserverRegistry ( ) ;
274299 await registry . start ( ) ;
275300 this . setState ( 'started' ) ;
@@ -288,6 +313,11 @@ export class Application extends Context implements LifeCycleObserver {
288313 // No-op if it's created or stopped
289314 if ( this . _state !== 'started' ) return ;
290315 this . setState ( 'stopping' ) ;
316+ if ( ! this . _isShuttingDown ) {
317+ // Explicit stop is called, let's remove signal listeners to avoid
318+ // memory leak and max listener warning
319+ this . removeSignalListener ( ) ;
320+ }
291321 const registry = await this . getLifeCycleObserverRegistry ( ) ;
292322 await registry . stop ( ) ;
293323 this . setState ( 'stopped' ) ;
@@ -320,7 +350,7 @@ export class Application extends Context implements LifeCycleObserver {
320350 * ```
321351 */
322352 public component ( componentCtor : Constructor < Component > , name ?: string ) {
323- debug ( 'Adding component: %s' , name ?? componentCtor . name ) ;
353+ this . debug ( 'Adding component: %s' , name ?? componentCtor . name ) ;
324354 const binding = createBindingFromClass ( componentCtor , {
325355 name,
326356 namespace : CoreBindings . COMPONENTS ,
@@ -356,7 +386,7 @@ export class Application extends Context implements LifeCycleObserver {
356386 ctor : Constructor < T > ,
357387 name ?: string ,
358388 ) : Binding < T > {
359- debug ( 'Adding life cycle observer %s' , name ?? ctor . name ) ;
389+ this . debug ( 'Adding life cycle observer %s' , name ?? ctor . name ) ;
360390 const binding = createBindingFromClass ( ctor , {
361391 name,
362392 namespace : CoreBindings . LIFE_CYCLE_OBSERVERS ,
@@ -421,17 +451,24 @@ export class Application extends Context implements LifeCycleObserver {
421451
422452 /**
423453 * Set up signals that are captured to shutdown the application
424- * @param signals - An array of signals to be trapped
425- * @param gracePeriod - A grace period in ms before forced exit
426454 */
427- protected setupShutdown ( signals : NodeJS . Signals [ ] , gracePeriod ?: number ) {
428- const cleanup = async ( signal : string ) => {
455+ protected setupShutdown ( ) {
456+ if ( this . _signalListener != null ) {
457+ this . registerSignalListener ( ) ;
458+ return this . _signalListener ;
459+ }
460+ const gracePeriod = this . _shutdownOptions . gracePeriod ;
461+ this . _signalListener = async ( signal : string ) => {
429462 const kill = ( ) => {
430- // eslint-disable-next-line @typescript-eslint/no-misused-promises
431- signals . forEach ( sig => process . removeListener ( sig , cleanup ) ) ;
463+ this . removeSignalListener ( ) ;
432464 process . kill ( process . pid , signal ) ;
433465 } ;
434- debug ( 'Signal %s received for process %d' , signal , process . pid ) ;
466+ debugShutdown (
467+ '[%s] Signal %s received for process %d' ,
468+ this . name ,
469+ signal ,
470+ process . pid ,
471+ ) ;
435472 if ( ! this . _isShuttingDown ) {
436473 this . _isShuttingDown = true ;
437474 let timer ;
@@ -446,8 +483,49 @@ export class Application extends Context implements LifeCycleObserver {
446483 }
447484 }
448485 } ;
449- // eslint-disable-next-line @typescript-eslint/no-misused-promises
450- signals . forEach ( sig => process . on ( sig , cleanup ) ) ;
486+ this . registerSignalListener ( ) ;
487+ return this . _signalListener ;
488+ }
489+
490+ private registerSignalListener ( ) {
491+ const { signals = [ ] } = this . _shutdownOptions ;
492+ debugShutdown (
493+ '[%s] Registering signal listeners on the process %d' ,
494+ this . name ,
495+ process . pid ,
496+ signals ,
497+ ) ;
498+ signals . forEach ( sig => {
499+ if ( process . getMaxListeners ( ) <= process . listenerCount ( sig ) ) {
500+ if ( debugWarning . enabled ) {
501+ debugWarning (
502+ '[%s] %d %s listeners are added to process %d' ,
503+ this . name ,
504+ process . listenerCount ( sig ) ,
505+ sig ,
506+ process . pid ,
507+ new Error ( 'MaxListenersExceededWarning' ) ,
508+ ) ;
509+ }
510+ }
511+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
512+ process . on ( sig , this . _signalListener ) ;
513+ } ) ;
514+ }
515+
516+ private removeSignalListener ( ) {
517+ if ( this . _signalListener == null ) return ;
518+ const { signals = [ ] } = this . _shutdownOptions ;
519+ debugShutdown (
520+ '[%s] Removing signal listeners on the process %d' ,
521+ this . name ,
522+ process . pid ,
523+ signals ,
524+ ) ;
525+ signals . forEach ( sig =>
526+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
527+ process . removeListener ( sig , this . _signalListener ) ,
528+ ) ;
451529 }
452530}
453531
@@ -470,6 +548,10 @@ export type ShutdownOptions = {
470548 * Configuration for application
471549 */
472550export interface ApplicationConfig {
551+ /**
552+ * Name of the application context
553+ */
554+ name ?: string ;
473555 /**
474556 * Configuration for signals that shut down the application
475557 */
0 commit comments