diff --git a/src/node/heart.ts b/src/node/heart.ts index a03802035..19f9aa4ad 100644 --- a/src/node/heart.ts +++ b/src/node/heart.ts @@ -9,7 +9,10 @@ export class Heart { private heartbeatInterval = 60000 public lastHeartbeat = 0 - public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise) {} + public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise) { + this.beat = this.beat.bind(this) + this.alive = this.alive.bind(this) + } public alive(): boolean { const now = Date.now() @@ -20,20 +23,22 @@ export class Heart { * timeout and start or reset a timer that keeps running as long as there is * activity. Failures are logged as warnings. */ - public beat(): void { + public async beat(): Promise { if (this.alive()) { return } logger.trace("heartbeat") - fs.writeFile(this.heartbeatPath, "").catch((error) => { - logger.warn(error.message) - }) this.lastHeartbeat = Date.now() if (typeof this.heartbeatTimer !== "undefined") { clearTimeout(this.heartbeatTimer) } this.heartbeatTimer = setTimeout(() => heartbeatTimer(this.isActive, this.beat), this.heartbeatInterval) + try { + return await fs.writeFile(this.heartbeatPath, "") + } catch (error: any) { + logger.warn(error.message) + } } /** diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 71ce557c5..13d53df86 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -56,6 +56,8 @@ export const register = async (app: App, args: DefaultedArgs): Promise { }) afterEach(() => { jest.resetAllMocks() + jest.useRealTimers() if (heart) { heart.dispose() } @@ -42,11 +43,7 @@ describe("Heart", () => { expect(fileContents).toBe(text) heart = new Heart(pathToFile, mockIsActive(true)) - heart.beat() - // HACK@jsjoeio - beat has some async logic but is not an async method - // Therefore, we have to create an artificial wait in order to make sure - // all async code has completed before asserting - await new Promise((r) => setTimeout(r, 100)) + await heart.beat() // Check that the heart wrote to the heartbeatFilePath and overwrote our text const fileContentsAfterBeat = await readFile(pathToFile, { encoding: "utf8" }) expect(fileContentsAfterBeat).not.toBe(text) @@ -56,15 +53,11 @@ describe("Heart", () => { }) it("should log a warning when given an invalid file path", async () => { heart = new Heart(`fakeDir/fake.txt`, mockIsActive(false)) - heart.beat() - // HACK@jsjoeio - beat has some async logic but is not an async method - // Therefore, we have to create an artificial wait in order to make sure - // all async code has completed before asserting - await new Promise((r) => setTimeout(r, 100)) + await heart.beat() expect(logger.warn).toHaveBeenCalled() }) - it("should be active after calling beat", () => { - heart.beat() + it("should be active after calling beat", async () => { + await heart.beat() const isAlive = heart.alive() expect(isAlive).toBe(true) @@ -75,6 +68,17 @@ describe("Heart", () => { const isAlive = heart.alive() expect(isAlive).toBe(false) }) + it("should beat twice without warnings", async () => { + // Use fake timers so we can speed up setTimeout + jest.useFakeTimers() + heart = new Heart(`${testDir}/hello.txt`, mockIsActive(true)) + await heart.beat() + // we need to speed up clocks, timeouts + // call heartbeat again (and it won't be alive I think) + // then assert no warnings were called + jest.runAllTimers() + expect(logger.warn).not.toHaveBeenCalled() + }) }) describe("heartbeatTimer", () => {