@@ -184,6 +184,7 @@ export class InternalChannel {
184184 private callCount = 0 ;
185185 private idleTimer : NodeJS . Timeout | null = null ;
186186 private readonly idleTimeoutMs : number ;
187+ private lastActivityTimestamp : Date ;
187188
188189 // Channelz info
189190 private readonly channelzEnabled : boolean = true ;
@@ -409,6 +410,7 @@ export class InternalChannel {
409410 'Channel constructed \n' +
410411 error . stack ?. substring ( error . stack . indexOf ( '\n' ) + 1 )
411412 ) ;
413+ this . lastActivityTimestamp = new Date ( ) ;
412414 }
413415
414416 private getChannelzInfo ( ) : ChannelInfo {
@@ -556,19 +558,44 @@ export class InternalChannel {
556558 this . resolvingLoadBalancer . destroy ( ) ;
557559 this . updateState ( ConnectivityState . IDLE ) ;
558560 this . currentPicker = new QueuePicker ( this . resolvingLoadBalancer ) ;
561+ if ( this . idleTimer ) {
562+ clearTimeout ( this . idleTimer ) ;
563+ this . idleTimer = null ;
564+ }
559565 }
560566
561- private maybeStartIdleTimer ( ) {
562- if ( this . connectivityState !== ConnectivityState . SHUTDOWN && this . callCount === 0 ) {
563- this . idleTimer = setTimeout ( ( ) => {
567+ private startIdleTimeout ( timeoutMs : number ) {
568+ this . idleTimer = setTimeout ( ( ) => {
569+ if ( this . callCount > 0 ) {
570+ /* If there is currently a call, the channel will not go idle for a
571+ * period of at least idleTimeoutMs, so check again after that time.
572+ */
573+ this . startIdleTimeout ( this . idleTimeoutMs ) ;
574+ return ;
575+ }
576+ const now = new Date ( ) ;
577+ const timeSinceLastActivity = now . valueOf ( ) - this . lastActivityTimestamp . valueOf ( ) ;
578+ if ( timeSinceLastActivity >= this . idleTimeoutMs ) {
564579 this . trace (
565580 'Idle timer triggered after ' +
566581 this . idleTimeoutMs +
567582 'ms of inactivity'
568583 ) ;
569584 this . enterIdle ( ) ;
570- } , this . idleTimeoutMs ) ;
571- this . idleTimer . unref ?.( ) ;
585+ } else {
586+ /* Whenever the timer fires with the latest activity being too recent,
587+ * set the timer again for the time when the time since the last
588+ * activity is equal to the timeout. This should result in the timer
589+ * firing no more than once every idleTimeoutMs/2 on average. */
590+ this . startIdleTimeout ( this . idleTimeoutMs - timeSinceLastActivity ) ;
591+ }
592+ } , timeoutMs ) ;
593+ this . idleTimer . unref ?.( ) ;
594+ }
595+
596+ private maybeStartIdleTimer ( ) {
597+ if ( this . connectivityState !== ConnectivityState . SHUTDOWN && ! this . idleTimer ) {
598+ this . startIdleTimeout ( this . idleTimeoutMs ) ;
572599 }
573600 }
574601
@@ -577,10 +604,6 @@ export class InternalChannel {
577604 this . callTracker . addCallStarted ( ) ;
578605 }
579606 this . callCount += 1 ;
580- if ( this . idleTimer ) {
581- clearTimeout ( this . idleTimer ) ;
582- this . idleTimer = null ;
583- }
584607 }
585608
586609 private onCallEnd ( status : StatusObject ) {
@@ -592,6 +615,7 @@ export class InternalChannel {
592615 }
593616 }
594617 this . callCount -= 1 ;
618+ this . lastActivityTimestamp = new Date ( ) ;
595619 this . maybeStartIdleTimer ( ) ;
596620 }
597621
@@ -729,6 +753,7 @@ export class InternalChannel {
729753 const connectivityState = this . connectivityState ;
730754 if ( tryToConnect ) {
731755 this . resolvingLoadBalancer . exitIdle ( ) ;
756+ this . lastActivityTimestamp = new Date ( ) ;
732757 this . maybeStartIdleTimer ( ) ;
733758 }
734759 return connectivityState ;
0 commit comments