Skip to content

Add optional idle timeout shutdown #7177

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Update idle-timeout to have its own timer, use process.kill SIGTERM f…
…or shutdown
  • Loading branch information
gaberudy committed Jan 21, 2025
commit c884f4833841e9f42d02fb9d8929d75cebf223b5
39 changes: 21 additions & 18 deletions src/node/heart.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import { promises as fs } from "fs"
*/
export class Heart {
private heartbeatTimer?: NodeJS.Timeout
private idleCheckTimer?: NodeJS.Timeout
private heartbeatInterval = 60000
public lastHeartbeat = 0

@@ -16,6 +17,10 @@ export class Heart {
) {
this.beat = this.beat.bind(this)
this.alive = this.alive.bind(this)
// Start idle check timer if timeout is configured
if (this.idleTimeout) {
this.startIdleCheck()
}
}

public alive(): boolean {
@@ -37,24 +42,35 @@ export class Heart {
if (typeof this.heartbeatTimer !== "undefined") {
clearTimeout(this.heartbeatTimer)
}
this.heartbeatTimer = setTimeout(
() => heartbeatTimer(this.isActive, this.beat, this.lastHeartbeat, this.idleTimeout),
this.heartbeatInterval
)
this.heartbeatTimer = setTimeout(() => heartbeatTimer(this.isActive, this.beat), this.heartbeatInterval)
try {
return await fs.writeFile(this.heartbeatPath, "")
} catch (error: any) {
logger.warn(error.message)
}
}

private startIdleCheck(): void {
// Check every minute if the idle timeout has been exceeded
this.idleCheckTimer = setInterval(() => {
const timeSinceLastBeat = Date.now() - this.lastHeartbeat
if (timeSinceLastBeat > this.idleTimeout! * 60 * 1000) {
logger.warn(`Idle timeout of ${this.idleTimeout} minutes exceeded`)
process.kill(process.pid, "SIGTERM")
}
}, 60000)
}

/**
* Call to clear any heartbeatTimer for shutdown.
*/
public dispose(): void {
if (typeof this.heartbeatTimer !== "undefined") {
clearTimeout(this.heartbeatTimer)
}
if (typeof this.idleCheckTimer !== "undefined") {
clearInterval(this.idleCheckTimer)
}
}
}

@@ -65,21 +81,8 @@ export class Heart {
*
* Extracted to make it easier to test.
*/
export async function heartbeatTimer(
isActive: Heart["isActive"],
beat: Heart["beat"],
lastHeartbeat: number,
idleTimeout?: number,
) {
export async function heartbeatTimer(isActive: Heart["isActive"], beat: Heart["beat"]) {
try {
// Check for idle timeout first
if (idleTimeout) {
const timeSinceLastBeat = Date.now() - lastHeartbeat
if (timeSinceLastBeat > idleTimeout * 60 * 1000) {
logger.warn(`Idle timeout of ${idleTimeout} minutes exceeded`)
process.exit(0)
}
}
if (await isActive()) {
beat()
}
4 changes: 2 additions & 2 deletions test/unit/node/heart.test.ts
Original file line number Diff line number Diff line change
@@ -95,7 +95,7 @@ describe("heartbeatTimer", () => {
const isActive = true
const mockIsActive = jest.fn().mockResolvedValue(isActive)
const mockBeatFn = jest.fn()
await heartbeatTimer(mockIsActive, mockBeatFn, 0)
await heartbeatTimer(mockIsActive, mockBeatFn)
expect(mockIsActive).toHaveBeenCalled()
expect(mockBeatFn).toHaveBeenCalled()
})
@@ -104,7 +104,7 @@ describe("heartbeatTimer", () => {
const error = new Error(errorMsg)
const mockIsActive = jest.fn().mockRejectedValue(error)
const mockBeatFn = jest.fn()
await heartbeatTimer(mockIsActive, mockBeatFn, 0)
await heartbeatTimer(mockIsActive, mockBeatFn)
expect(mockIsActive).toHaveBeenCalled()
expect(mockBeatFn).not.toHaveBeenCalled()
expect(logger.warn).toHaveBeenCalledWith(errorMsg)