feat: Add Finalizable interface

This commit is contained in:
Joachim Van Herwegen 2021-06-11 15:38:02 +02:00
parent 93374f011a
commit 29ddf57341
6 changed files with 52 additions and 14 deletions

View File

@ -1,13 +1,18 @@
import type { Server } from 'http';
import { promisify } from 'util';
import type { HttpServerFactory } from '../server/HttpServerFactory';
import type { Finalizable } from './final/Finalizable';
import { Initializer } from './Initializer';
/**
* Creates and starts an HTTP server.
*/
export class ServerInitializer extends Initializer {
export class ServerInitializer extends Initializer implements Finalizable {
private readonly serverFactory: HttpServerFactory;
private readonly port: number;
private server?: Server;
public constructor(serverFactory: HttpServerFactory, port: number) {
super();
this.serverFactory = serverFactory;
@ -15,6 +20,12 @@ export class ServerInitializer extends Initializer {
}
public async handle(): Promise<void> {
this.serverFactory.startServer(this.port);
this.server = this.serverFactory.startServer(this.port);
}
public async finalize(): Promise<void> {
if (this.server) {
return promisify(this.server.close.bind(this.server))();
}
}
}

View File

@ -0,0 +1,6 @@
/**
* Allows for cleaning up an object and stopping relevant loops when the application needs to be stopped.
*/
export interface Finalizable {
finalize: () => Promise<void>;
}

View File

@ -1,3 +1,4 @@
import type { Finalizable } from '../../init/final/Finalizable';
import { getLoggerFor } from '../../logging/LogUtil';
import { InternalServerError } from '../../util/errors/InternalServerError';
import type { ExpiringStorage } from './ExpiringStorage';
@ -11,9 +12,8 @@ export type Expires<T> = { expires?: string; payload: T };
* Will delete expired entries when trying to get their value.
* Has a timer that will delete all expired data every hour (default value).
*/
export class WrappedExpiringStorage<TKey, TValue> implements ExpiringStorage<TKey, TValue> {
export class WrappedExpiringStorage<TKey, TValue> implements ExpiringStorage<TKey, TValue>, Finalizable {
protected readonly logger = getLoggerFor(this);
private readonly source: KeyValueStorage<TKey, Expires<TValue>>;
private readonly timer: NodeJS.Timeout;
@ -118,7 +118,7 @@ export class WrappedExpiringStorage<TKey, TValue> implements ExpiringStorage<TKe
/**
* Stops the continuous cleanup timer.
*/
public finalize(): void {
public async finalize(): Promise<void> {
clearInterval(this.timer);
}
}

View File

@ -3,6 +3,7 @@ import type { RedisClient } from 'redis';
import { createClient } from 'redis';
import type { Lock } from 'redlock';
import Redlock from 'redlock';
import type { Finalizable } from '../../init/final/Finalizable';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../../logging/LogUtil';
import { InternalServerError } from '../errors/InternalServerError';
@ -37,7 +38,7 @@ const defaultRedlockConfig = {
* in that sense it is kind of multithreaded.
* - Redlock does not provide the ability to see which locks have expired
*/
export class RedisResourceLocker implements ResourceLocker {
export class RedisResourceLocker implements ResourceLocker, Finalizable {
protected readonly logger = getLoggerFor(this);
private readonly redlock: Redlock;
@ -100,10 +101,13 @@ export class RedisResourceLocker implements ResourceLocker {
public async finalize(): Promise<void> {
// This for loop is an extra failsafe,
// this extra code won't slow down anything, this function will only be called to shut down in peace
for (const [ , { lock }] of this.lockMap.entries()) {
await this.release({ path: lock.resource });
try {
for (const [ , { lock }] of this.lockMap.entries()) {
await this.release({ path: lock.resource });
}
} finally {
await this.redlock.quit();
}
await this.redlock.quit();
}
public async acquire(identifier: ResourceIdentifier): Promise<void> {

View File

@ -1,13 +1,19 @@
import type { Server } from 'http';
import { ServerInitializer } from '../../../src/init/ServerInitializer';
import type { HttpServerFactory } from '../../../src/server/HttpServerFactory';
describe('ServerInitializer', (): void => {
const serverFactory: jest.Mocked<HttpServerFactory> = {
startServer: jest.fn(),
};
let server: Server;
let serverFactory: jest.Mocked<HttpServerFactory>;
let initializer: ServerInitializer;
beforeAll(async(): Promise<void> => {
beforeEach(async(): Promise<void> => {
server = {
close: jest.fn((fn: () => void): void => fn()),
} as any;
serverFactory = {
startServer: jest.fn().mockReturnValue(server),
};
initializer = new ServerInitializer(serverFactory, 3000);
});
@ -15,4 +21,15 @@ describe('ServerInitializer', (): void => {
await initializer.handle();
expect(serverFactory.startServer).toHaveBeenCalledWith(3000);
});
it('can stop the server.', async(): Promise<void> => {
await initializer.handle();
await expect(initializer.finalize()).resolves.toBeUndefined();
expect(server.close).toHaveBeenCalledTimes(1);
});
it('only tries to stop the server if it was initialized.', async(): Promise<void> => {
await expect(initializer.finalize()).resolves.toBeUndefined();
expect(server.close).toHaveBeenCalledTimes(0);
});
});

View File

@ -154,7 +154,7 @@ describe('A WrappedExpiringStorage', (): void => {
yield* data;
});
expect(storage.finalize()).toBeUndefined();
await expect(storage.finalize()).resolves.toBeUndefined();
// Make sure clearInterval was called with the interval timer
expect(mockClear.mock.calls).toHaveLength(1);