handle errors from cache management callbacks
This commit is contained in:
parent
4336a5d214
commit
151550602c
3 changed files with 189 additions and 10 deletions
|
|
@ -18,6 +18,7 @@ import { bindThis } from '@/decorators.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import { TimeService, type TimerHandle } from '@/global/TimeService.js';
|
||||
import { InternalEventService } from '@/global/InternalEventService.js';
|
||||
import { callAllOn } from '@/misc/call-all.js';
|
||||
|
||||
// This is the one place that's *supposed* to new() up caches.
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
|
|
@ -97,29 +98,25 @@ export class CacheManagementService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
public gc(): void {
|
||||
this.resetGcTimer(() => {
|
||||
// TODO callAll()
|
||||
for (const manager of this.managedCaches) {
|
||||
manager.gc();
|
||||
}
|
||||
callAllOn(this.managedCaches, 'gc');
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public clear(): void {
|
||||
this.resetGcTimer(() => {
|
||||
for (const manager of this.managedCaches) {
|
||||
manager.clear();
|
||||
}
|
||||
callAllOn(this.managedCaches, 'clear');
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async dispose(): Promise<void> {
|
||||
this.stopGcTimer();
|
||||
for (const manager of this.managedCaches) {
|
||||
manager.dispose();
|
||||
}
|
||||
|
||||
const toDispose = new Set(this.managedCaches);
|
||||
this.managedCaches.clear();
|
||||
|
||||
callAllOn(toDispose, 'dispose');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
59
packages/backend/src/misc/call-all.ts
Normal file
59
packages/backend/src/misc/call-all.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calls a group of functions with the given parameters.
|
||||
* Errors are suppressed and aggregated, ensuring that nothing is thrown until all calls have completed.
|
||||
* This ensures that an error in one callback does not prevent later callbacks from completing.
|
||||
* @param funcs Callback functions to execute
|
||||
* @param args Arguments to pass to each callback
|
||||
*/
|
||||
export function callAll<T extends unknown[]>(funcs: Iterable<(...args: T) => void>, ...args: T): void {
|
||||
const errors: unknown[] = [];
|
||||
|
||||
for (const func of funcs) {
|
||||
try {
|
||||
func(...args);
|
||||
} catch (err) {
|
||||
errors.push(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new AggregateError(errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a single method across a group of object, passing the given parameters as values.
|
||||
* Errors are suppressed and aggregated, ensuring that nothing is thrown until all calls have completed.
|
||||
* This ensures that an error in one callback does not prevent later callbacks from completing.
|
||||
* @param objects Objects to execute methods on
|
||||
* @param method Name (property key) of the method to execute
|
||||
* @param args Arguments to pass
|
||||
*/
|
||||
export function callAllOn<TObject, TMethod extends MethodKeys<TObject>>(objects: Iterable<TObject>, method: TMethod, ...args: MethodParams<TObject, TMethod>): void {
|
||||
const errors: unknown[] = [];
|
||||
|
||||
for (const object of objects) {
|
||||
try {
|
||||
// @ts-expect-error Our generic constraints ensure this is safe, but TS can't infer that much context.
|
||||
object[method](...args);
|
||||
} catch (err) {
|
||||
errors.push(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new AggregateError(errors);
|
||||
}
|
||||
}
|
||||
|
||||
type AnyFunc = (...args: unknown[]) => unknown;
|
||||
type Methods<TObject> = {
|
||||
[Key in keyof TObject]: TObject[Key] extends AnyFunc ? TObject[Key] : never;
|
||||
};
|
||||
type MethodKeys<TObject> = keyof Methods<TObject>;
|
||||
type MethodParams<TObject, TMethod extends MethodKeys<TObject>> = TObject[TMethod] extends AnyFunc ? Parameters<TObject[TMethod]> : never;
|
||||
123
packages/backend/test/unit/misc/call-all-tests.ts
Normal file
123
packages/backend/test/unit/misc/call-all-tests.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import { callAll, callAllOn } from '@/misc/call-all.js';
|
||||
|
||||
describe(callAll, () => {
|
||||
it('should call all functions when all succeed', () => {
|
||||
const funcs = [
|
||||
jest.fn(() => {}),
|
||||
jest.fn(() => {}),
|
||||
jest.fn(() => {}),
|
||||
];
|
||||
|
||||
callAll(funcs);
|
||||
|
||||
for (const func of funcs) {
|
||||
expect(func).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('should pass parameters to all functions', () => {
|
||||
const funcs = [
|
||||
jest.fn((num: number) => expect(num).toBe(1)),
|
||||
jest.fn((num: number) => expect(num).toBe(1)),
|
||||
jest.fn((num: number) => expect(num).toBe(1)),
|
||||
];
|
||||
|
||||
callAll(funcs, 1);
|
||||
});
|
||||
|
||||
it('should call all functions when some fail', () => {
|
||||
const funcs = [
|
||||
jest.fn(() => { throw new Error(); }),
|
||||
jest.fn(() => {}),
|
||||
jest.fn(() => {}),
|
||||
];
|
||||
|
||||
try {
|
||||
callAll(funcs);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
for (const func of funcs) {
|
||||
expect(func).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw when some functions fail', () => {
|
||||
const funcs = [
|
||||
jest.fn(() => { throw new Error(); }),
|
||||
jest.fn(() => {}),
|
||||
jest.fn(() => {}),
|
||||
];
|
||||
|
||||
expect(() => callAll(funcs)).toThrow();
|
||||
});
|
||||
|
||||
it('should not throw when input is empty', () => {
|
||||
expect(() => callAll([])).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe(callAllOn, () => {
|
||||
it('should call all methods when all succeed', () => {
|
||||
const objects = [
|
||||
{ foo: jest.fn(() => {}) },
|
||||
{ foo: jest.fn(() => {}) },
|
||||
{ foo: jest.fn(() => {}) },
|
||||
];
|
||||
|
||||
callAllOn(objects, 'foo');
|
||||
|
||||
for (const object of objects) {
|
||||
expect(object.foo).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('should pass parameters to all methods', () => {
|
||||
const objects = [
|
||||
{ foo: jest.fn((num: number) => expect(num).toBe(1)) },
|
||||
{ foo: jest.fn((num: number) => expect(num).toBe(1)) },
|
||||
{ foo: jest.fn((num: number) => expect(num).toBe(1)) },
|
||||
];
|
||||
|
||||
callAllOn(objects, 'foo', 1);
|
||||
});
|
||||
|
||||
it('should call all methods when some fail', () => {
|
||||
const objects = [
|
||||
{ foo: jest.fn(() => {}) },
|
||||
{ foo: jest.fn(() => {}) },
|
||||
{ foo: jest.fn(() => {}) },
|
||||
];
|
||||
|
||||
try {
|
||||
callAllOn(objects, 'foo');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
for (const object of objects) {
|
||||
expect(object.foo).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw when some methods fail', () => {
|
||||
const objects = [
|
||||
{ foo: jest.fn(() => { throw new Error(); }) },
|
||||
{ foo: jest.fn(() => {}) },
|
||||
{ foo: jest.fn(() => {}) },
|
||||
];
|
||||
|
||||
expect(() => callAllOn(objects, 'foo')).toThrow();
|
||||
});
|
||||
|
||||
it('should not throw when input is empty', () => {
|
||||
expect(() => callAllOn([] as { foo: () => void }[], 'foo')).not.toThrow();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue