Skip to content

Commit 88e971c

Browse files
authored
refactor(heart): bind class methods and make beat async (#5142)
* feat: set up new test for beat twice * refactor: make Heart.beat() async This allows us to properly await heart.beat() in our tests and remove the HACK I added before. * refactor: bind heart methods .beat and .alive This allows the functions to maintain access to the Heart instance (or `this`) even when they are passed to other functions. We do this because we pass both `isActive` and `beat` to `heartbeatTimer`. * feat(heart): add test to ensure no warnings called * fixup!: revert setTimeout for heartbeatTimer * fixup!: return promise in beat
1 parent 7027ec7 commit 88e971c

File tree

3 files changed

+28
-17
lines changed

3 files changed

+28
-17
lines changed

src/node/heart.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ export class Heart {
99
private heartbeatInterval = 60000
1010
public lastHeartbeat = 0
1111

12-
public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {}
12+
public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {
13+
this.beat = this.beat.bind(this)
14+
this.alive = this.alive.bind(this)
15+
}
1316

1417
public alive(): boolean {
1518
const now = Date.now()
@@ -20,20 +23,22 @@ export class Heart {
2023
* timeout and start or reset a timer that keeps running as long as there is
2124
* activity. Failures are logged as warnings.
2225
*/
23-
public beat(): void {
26+
public async beat(): Promise<void> {
2427
if (this.alive()) {
2528
return
2629
}
2730

2831
logger.trace("heartbeat")
29-
fs.writeFile(this.heartbeatPath, "").catch((error) => {
30-
logger.warn(error.message)
31-
})
3232
this.lastHeartbeat = Date.now()
3333
if (typeof this.heartbeatTimer !== "undefined") {
3434
clearTimeout(this.heartbeatTimer)
3535
}
3636
this.heartbeatTimer = setTimeout(() => heartbeatTimer(this.isActive, this.beat), this.heartbeatInterval)
37+
try {
38+
return await fs.writeFile(this.heartbeatPath, "")
39+
} catch (error: any) {
40+
logger.warn(error.message)
41+
}
3742
}
3843

3944
/**

src/node/routes/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
5656
// /healthz|/healthz/ needs to be excluded otherwise health checks will make
5757
// it look like code-server is always in use.
5858
if (!/^\/healthz\/?$/.test(req.url)) {
59+
// NOTE@jsjoeio - intentionally not awaiting the .beat() call here because
60+
// we don't want to slow down the request.
5961
heart.beat()
6062
}
6163

test/unit/node/heart.test.ts

+16-12
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe("Heart", () => {
2323
})
2424
afterEach(() => {
2525
jest.resetAllMocks()
26+
jest.useRealTimers()
2627
if (heart) {
2728
heart.dispose()
2829
}
@@ -42,11 +43,7 @@ describe("Heart", () => {
4243
expect(fileContents).toBe(text)
4344

4445
heart = new Heart(pathToFile, mockIsActive(true))
45-
heart.beat()
46-
// HACK@jsjoeio - beat has some async logic but is not an async method
47-
// Therefore, we have to create an artificial wait in order to make sure
48-
// all async code has completed before asserting
49-
await new Promise((r) => setTimeout(r, 100))
46+
await heart.beat()
5047
// Check that the heart wrote to the heartbeatFilePath and overwrote our text
5148
const fileContentsAfterBeat = await readFile(pathToFile, { encoding: "utf8" })
5249
expect(fileContentsAfterBeat).not.toBe(text)
@@ -56,15 +53,11 @@ describe("Heart", () => {
5653
})
5754
it("should log a warning when given an invalid file path", async () => {
5855
heart = new Heart(`fakeDir/fake.txt`, mockIsActive(false))
59-
heart.beat()
60-
// HACK@jsjoeio - beat has some async logic but is not an async method
61-
// Therefore, we have to create an artificial wait in order to make sure
62-
// all async code has completed before asserting
63-
await new Promise((r) => setTimeout(r, 100))
56+
await heart.beat()
6457
expect(logger.warn).toHaveBeenCalled()
6558
})
66-
it("should be active after calling beat", () => {
67-
heart.beat()
59+
it("should be active after calling beat", async () => {
60+
await heart.beat()
6861

6962
const isAlive = heart.alive()
7063
expect(isAlive).toBe(true)
@@ -75,6 +68,17 @@ describe("Heart", () => {
7568
const isAlive = heart.alive()
7669
expect(isAlive).toBe(false)
7770
})
71+
it("should beat twice without warnings", async () => {
72+
// Use fake timers so we can speed up setTimeout
73+
jest.useFakeTimers()
74+
heart = new Heart(`${testDir}/hello.txt`, mockIsActive(true))
75+
await heart.beat()
76+
// we need to speed up clocks, timeouts
77+
// call heartbeat again (and it won't be alive I think)
78+
// then assert no warnings were called
79+
jest.runAllTimers()
80+
expect(logger.warn).not.toHaveBeenCalled()
81+
})
7882
})
7983

8084
describe("heartbeatTimer", () => {

0 commit comments

Comments
 (0)