Add idle timeout (#7539)

This commit is contained in:
Andrew Baldwin
2025-10-28 20:10:56 -04:00
committed by GitHub
parent 811ec6c1d6
commit db8a41bce1
7 changed files with 153 additions and 49 deletions

View File

@@ -1,6 +1,6 @@
import { logger } from "@coder/logger"
import { readFile, writeFile, stat, utimes } from "fs/promises"
import { Heart, heartbeatTimer } from "../../../src/node/heart"
import { Heart } from "../../../src/node/heart"
import { clean, mockLogger, tmpdir } from "../../utils/helpers"
const mockIsActive = (resolveTo: boolean) => jest.fn().mockResolvedValue(resolveTo)
@@ -82,7 +82,52 @@ describe("Heart", () => {
})
describe("heartbeatTimer", () => {
beforeAll(() => {
const testName = "heartbeatTimer"
let testDir = ""
beforeAll(async () => {
await clean(testName)
testDir = await tmpdir(testName)
mockLogger()
})
afterAll(() => {
jest.restoreAllMocks()
})
beforeEach(() => {
jest.useFakeTimers()
})
afterEach(() => {
jest.resetAllMocks()
jest.clearAllTimers()
jest.useRealTimers()
})
it("should call isActive when timeout expires", async () => {
const isActive = true
const mockIsActive = jest.fn().mockResolvedValue(isActive)
const heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive)
await heart.beat()
jest.advanceTimersByTime(60 * 1000)
expect(mockIsActive).toHaveBeenCalled()
})
it("should log a warning when isActive rejects", async () => {
const errorMsg = "oh no"
const error = new Error(errorMsg)
const mockIsActive = jest.fn().mockRejectedValue(error)
const heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive)
await heart.beat()
jest.advanceTimersByTime(60 * 1000)
expect(mockIsActive).toHaveBeenCalled()
expect(logger.warn).toHaveBeenCalledWith(errorMsg)
})
})
describe("stateChange", () => {
const testName = "stateChange"
let testDir = ""
let heart: Heart
beforeAll(async () => {
await clean(testName)
testDir = await tmpdir(testName)
mockLogger()
})
afterAll(() => {
@@ -90,23 +135,28 @@ describe("heartbeatTimer", () => {
})
afterEach(() => {
jest.resetAllMocks()
if (heart) {
heart.dispose()
}
})
it("should call beat when isActive resolves to true", async () => {
const isActive = true
const mockIsActive = jest.fn().mockResolvedValue(isActive)
const mockBeatFn = jest.fn()
await heartbeatTimer(mockIsActive, mockBeatFn)
expect(mockIsActive).toHaveBeenCalled()
expect(mockBeatFn).toHaveBeenCalled()
it("should change to alive after a beat", async () => {
heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive(true))
const mockOnChange = jest.fn()
heart.onChange(mockOnChange)
await heart.beat()
expect(mockOnChange.mock.calls[0][0]).toBe("alive")
})
it("should log a warning when isActive rejects", async () => {
const errorMsg = "oh no"
const error = new Error(errorMsg)
const mockIsActive = jest.fn().mockRejectedValue(error)
const mockBeatFn = jest.fn()
await heartbeatTimer(mockIsActive, mockBeatFn)
expect(mockIsActive).toHaveBeenCalled()
expect(mockBeatFn).not.toHaveBeenCalled()
expect(logger.warn).toHaveBeenCalledWith(errorMsg)
it.only("should change to expired when not active", async () => {
jest.useFakeTimers()
heart = new Heart(`${testDir}/shutdown.txt`, () => new Promise((resolve) => resolve(false)))
const mockOnChange = jest.fn()
heart.onChange(mockOnChange)
await heart.beat()
await jest.advanceTimersByTime(60 * 1000)
expect(mockOnChange.mock.calls[1][0]).toBe("expired")
jest.clearAllTimers()
jest.useRealTimers()
})
})

View File

@@ -16,6 +16,7 @@ jest.mock("@coder/logger", () => ({
debug: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
named: jest.fn(),
level: 0,
},
field: jest.fn(),
@@ -94,7 +95,7 @@ describe("main", () => {
// Mock routes module
jest.doMock("../../../src/node/routes", () => ({
register: jest.fn().mockResolvedValue(jest.fn()),
register: jest.fn().mockResolvedValue({ disposeRoutes: jest.fn() }),
}))
// Mock loadCustomStrings to succeed
@@ -131,7 +132,7 @@ describe("main", () => {
// Mock routes module
jest.doMock("../../../src/node/routes", () => ({
register: jest.fn().mockResolvedValue(jest.fn()),
register: jest.fn().mockResolvedValue({ disposeRoutes: jest.fn() }),
}))
// Import runCodeServer after mocking