mirror of https://github.com/coder/code-server.git
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
This commit is contained in:
parent
7027ec7d60
commit
88e971c609
|
@ -9,7 +9,10 @@ export class Heart {
|
||||||
private heartbeatInterval = 60000
|
private heartbeatInterval = 60000
|
||||||
public lastHeartbeat = 0
|
public lastHeartbeat = 0
|
||||||
|
|
||||||
public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {}
|
public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {
|
||||||
|
this.beat = this.beat.bind(this)
|
||||||
|
this.alive = this.alive.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
public alive(): boolean {
|
public alive(): boolean {
|
||||||
const now = Date.now()
|
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
|
* timeout and start or reset a timer that keeps running as long as there is
|
||||||
* activity. Failures are logged as warnings.
|
* activity. Failures are logged as warnings.
|
||||||
*/
|
*/
|
||||||
public beat(): void {
|
public async beat(): Promise<void> {
|
||||||
if (this.alive()) {
|
if (this.alive()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.trace("heartbeat")
|
logger.trace("heartbeat")
|
||||||
fs.writeFile(this.heartbeatPath, "").catch((error) => {
|
|
||||||
logger.warn(error.message)
|
|
||||||
})
|
|
||||||
this.lastHeartbeat = Date.now()
|
this.lastHeartbeat = Date.now()
|
||||||
if (typeof this.heartbeatTimer !== "undefined") {
|
if (typeof this.heartbeatTimer !== "undefined") {
|
||||||
clearTimeout(this.heartbeatTimer)
|
clearTimeout(this.heartbeatTimer)
|
||||||
}
|
}
|
||||||
this.heartbeatTimer = setTimeout(() => heartbeatTimer(this.isActive, this.beat), 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -56,6 +56,8 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
|
||||||
// /healthz|/healthz/ needs to be excluded otherwise health checks will make
|
// /healthz|/healthz/ needs to be excluded otherwise health checks will make
|
||||||
// it look like code-server is always in use.
|
// it look like code-server is always in use.
|
||||||
if (!/^\/healthz\/?$/.test(req.url)) {
|
if (!/^\/healthz\/?$/.test(req.url)) {
|
||||||
|
// NOTE@jsjoeio - intentionally not awaiting the .beat() call here because
|
||||||
|
// we don't want to slow down the request.
|
||||||
heart.beat()
|
heart.beat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ describe("Heart", () => {
|
||||||
})
|
})
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.resetAllMocks()
|
jest.resetAllMocks()
|
||||||
|
jest.useRealTimers()
|
||||||
if (heart) {
|
if (heart) {
|
||||||
heart.dispose()
|
heart.dispose()
|
||||||
}
|
}
|
||||||
|
@ -42,11 +43,7 @@ describe("Heart", () => {
|
||||||
expect(fileContents).toBe(text)
|
expect(fileContents).toBe(text)
|
||||||
|
|
||||||
heart = new Heart(pathToFile, mockIsActive(true))
|
heart = new Heart(pathToFile, mockIsActive(true))
|
||||||
heart.beat()
|
await 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))
|
|
||||||
// Check that the heart wrote to the heartbeatFilePath and overwrote our text
|
// Check that the heart wrote to the heartbeatFilePath and overwrote our text
|
||||||
const fileContentsAfterBeat = await readFile(pathToFile, { encoding: "utf8" })
|
const fileContentsAfterBeat = await readFile(pathToFile, { encoding: "utf8" })
|
||||||
expect(fileContentsAfterBeat).not.toBe(text)
|
expect(fileContentsAfterBeat).not.toBe(text)
|
||||||
|
@ -56,15 +53,11 @@ describe("Heart", () => {
|
||||||
})
|
})
|
||||||
it("should log a warning when given an invalid file path", async () => {
|
it("should log a warning when given an invalid file path", async () => {
|
||||||
heart = new Heart(`fakeDir/fake.txt`, mockIsActive(false))
|
heart = new Heart(`fakeDir/fake.txt`, mockIsActive(false))
|
||||||
heart.beat()
|
await 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))
|
|
||||||
expect(logger.warn).toHaveBeenCalled()
|
expect(logger.warn).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
it("should be active after calling beat", () => {
|
it("should be active after calling beat", async () => {
|
||||||
heart.beat()
|
await heart.beat()
|
||||||
|
|
||||||
const isAlive = heart.alive()
|
const isAlive = heart.alive()
|
||||||
expect(isAlive).toBe(true)
|
expect(isAlive).toBe(true)
|
||||||
|
@ -75,6 +68,17 @@ describe("Heart", () => {
|
||||||
const isAlive = heart.alive()
|
const isAlive = heart.alive()
|
||||||
expect(isAlive).toBe(false)
|
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", () => {
|
describe("heartbeatTimer", () => {
|
||||||
|
|
Loading…
Reference in New Issue