Skip to content

Commit

Permalink
fix: sleep function memory leak (#5023)
Browse files Browse the repository at this point in the history
Fixes #4817
  • Loading branch information
spypsy authored Mar 8, 2024
1 parent b2af880 commit a72cfea
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 13 deletions.
29 changes: 16 additions & 13 deletions yarn-project/foundation/src/sleep/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ import { InterruptError } from '../errors/index.js';
* setTimeout(() =\> sleeper.interrupt(true), 1500); // Interrupt the sleep after 1.5 seconds
*/
export class InterruptibleSleep {
private interruptResolve: (shouldThrow: boolean) => void = () => {};
private interruptPromise = new Promise<boolean>(resolve => (this.interruptResolve = resolve));
private timeouts: NodeJS.Timeout[] = [];
private interrupts: Array<(shouldThrow: boolean) => void> = [];

/**
* Sleep for a specified amount of time in milliseconds.
Expand All @@ -33,13 +31,18 @@ export class InterruptibleSleep {
* @param ms - The number of milliseconds to sleep.
* @returns A Promise that resolves after the specified time has passed.
*/
public async sleep(ms: number) {
let timeout!: NodeJS.Timeout;
const promise = new Promise<boolean>(resolve => (timeout = setTimeout(() => resolve(false), ms)));
this.timeouts.push(timeout);
const shouldThrow = await Promise.race([promise, this.interruptPromise]);
clearTimeout(timeout);
this.timeouts.splice(this.timeouts.indexOf(timeout), 1);
public async sleep(ms: number): Promise<void> {
let interruptResolve: (shouldThrow: boolean) => void;
const interruptPromise = new Promise<boolean>(resolve => {
interruptResolve = resolve;
this.interrupts.push(resolve);
});

const timeoutPromise = new Promise<boolean>(resolve => setTimeout(() => resolve(false), ms));
const shouldThrow = await Promise.race([interruptPromise, timeoutPromise]);

this.interrupts = this.interrupts.filter(res => res !== interruptResolve);

if (shouldThrow) {
throw new InterruptError('Interrupted.');
}
Expand All @@ -52,9 +55,9 @@ export class InterruptibleSleep {
*
* @param sleepShouldThrow - A boolean value indicating whether the sleep operation should throw an error when interrupted. Default is false.
*/
public interrupt(sleepShouldThrow = false) {
this.interruptResolve(sleepShouldThrow);
this.interruptPromise = new Promise(resolve => (this.interruptResolve = resolve));
public interrupt(sleepShouldThrow = false): void {
this.interrupts.forEach(resolve => resolve(sleepShouldThrow));
this.interrupts = [];
}
}

Expand Down
38 changes: 38 additions & 0 deletions yarn-project/foundation/src/sleep/sleep.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { jest } from '@jest/globals';

import { InterruptError } from '../errors/index.js';
import { InterruptibleSleep } from './index.js';

describe('InterruptibleSleep', () => {
it('should sleep for 100ms', async () => {
const sleeper = new InterruptibleSleep();
const start = Date.now();
await sleeper.sleep(100);
const end = Date.now();
// -1 ms wiggle room for rounding errors
expect(end - start).toBeGreaterThanOrEqual(99);
});

it('can start multiple sleeps', async () => {
const sleeper = new InterruptibleSleep();
const start = Date.now();
await Promise.all([sleeper.sleep(100), sleeper.sleep(150)]);
const end = Date.now();
expect(end - start).toBeGreaterThanOrEqual(149);
});

it('can interrup multiple sleeps', async () => {
const stub = jest.fn();
const sleeper = new InterruptibleSleep();
const start = Date.now();
let end1;
const sleep1 = sleeper.sleep(100).then(() => {
end1 = Date.now();
});
const sleep2 = sleeper.sleep(150).then(stub);
setTimeout(() => sleeper.interrupt(true), 125);
await Promise.all([sleep1, sleep2]).catch(e => expect(e).toBeInstanceOf(InterruptError));
expect(end1! - start).toBeGreaterThanOrEqual(99);
expect(stub).not.toHaveBeenCalled();
});
});

0 comments on commit a72cfea

Please sign in to comment.