mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Introducing initializers for handling lock cleanup on start
This commit is contained in:
parent
affcb7a7b3
commit
1c65b06392
@ -39,7 +39,17 @@ The following changes are relevant for v4 custom configs that replaced certain f
|
|||||||
- The `IdentityProviderFactory` inputs have been extended.
|
- The `IdentityProviderFactory` inputs have been extended.
|
||||||
- `/identity/handler/provider-factory/identity.json`
|
- `/identity/handler/provider-factory/identity.json`
|
||||||
- LDP components have slightly changed so the preference parser is in a separate config file.
|
- LDP components have slightly changed so the preference parser is in a separate config file.
|
||||||
- `/config/ldp/handler/*`
|
- `/ldp/handler/*`
|
||||||
|
- Restructured the init configs.
|
||||||
|
- `/app/init/base/init.json`
|
||||||
|
- `/app/main/default.json`
|
||||||
|
- Added lock cleanup on server start (and updated existing finalization).
|
||||||
|
- `/util/resource-locker/file.json`
|
||||||
|
- `/util/resource-locker/redis.json`
|
||||||
|
- Updated finalizers.
|
||||||
|
- `/app/identity/handler/account-store/default.json`
|
||||||
|
- `/identity/ownership/token.json`
|
||||||
|
- `/ldp/authorization/readers/access-checkers/agent-group.json`
|
||||||
|
|
||||||
### Interface changes
|
### Interface changes
|
||||||
These changes are relevant if you wrote custom modules for the server that depend on existing interfaces.
|
These changes are relevant if you wrote custom modules for the server that depend on existing interfaces.
|
||||||
@ -49,6 +59,12 @@ These changes are relevant if you wrote custom modules for the server that depen
|
|||||||
- Both `TemplateEngine` implementations now take a `baseUrl` parameter as input.
|
- Both `TemplateEngine` implementations now take a `baseUrl` parameter as input.
|
||||||
- The `IdentityProviderFactory` and `ConvertingErrorHandler` now additionally take a `PreferenceParser` as input.
|
- The `IdentityProviderFactory` and `ConvertingErrorHandler` now additionally take a `PreferenceParser` as input.
|
||||||
- Error handlers now take the incoming HttpRequest as input instead of just the preferences.
|
- Error handlers now take the incoming HttpRequest as input instead of just the preferences.
|
||||||
|
- Extended the initialization/finalization system:
|
||||||
|
* Introduced `Initializable` interface and `InitializableHandler` wrapper class.
|
||||||
|
* Introduced `Finalizer` abstract class and `FinalizableHandler` wrapper class.
|
||||||
|
* Changed type for `finalizer` attribute in `App` from `Finalizable` to `Finalizer` and updated the calling code in `App.stop()`.
|
||||||
|
* Removed the now obsolete `ParallelFinalizer` util class.
|
||||||
|
- Added a lock cleanup on initialize for lock implementations `RedisLocker` and `FileSystemResourceLocker`.
|
||||||
|
|
||||||
A new interface `SingleThreaded` has been added. This empty interface can be implemented to mark a component as not-threadsafe. When the CSS starts in multithreaded mode, it will error and halt if any SingleThreaded components are instantiated.
|
A new interface `SingleThreaded` has been added. This empty interface can be implemented to mark a component as not-threadsafe. When the CSS starts in multithreaded mode, it will error and halt if any SingleThreaded components are instantiated.
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
"@id": "urn:solid-server:default:PrimarySequenceInitializer",
|
"@id": "urn:solid-server:default:PrimarySequenceInitializer",
|
||||||
"@type":"SequenceHandler",
|
"@type":"SequenceHandler",
|
||||||
"handlers": [
|
"handlers": [
|
||||||
|
{ "@id": "urn:solid-server:default:CleanupInitializer"},
|
||||||
{ "@id": "urn:solid-server:default:BaseUrlVerifier" },
|
{ "@id": "urn:solid-server:default:BaseUrlVerifier" },
|
||||||
{ "@id": "urn:solid-server:default:PrimaryParallelInitializer" },
|
{ "@id": "urn:solid-server:default:PrimaryParallelInitializer" },
|
||||||
{ "@id": "urn:solid-server:default:SeededPodInitializer" },
|
{ "@id": "urn:solid-server:default:SeededPodInitializer" },
|
||||||
@ -53,6 +54,13 @@
|
|||||||
{ "@id": "urn:solid-server:default:ServerInitializer" }
|
{ "@id": "urn:solid-server:default:ServerInitializer" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "Initializers that need to cleanup or do anything else before something writes to the backend should be added here.",
|
||||||
|
"@id": "urn:solid-server:default:CleanupInitializer",
|
||||||
|
"@type":"SequenceHandler",
|
||||||
|
"handlers": [
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -7,18 +7,36 @@
|
|||||||
"@type": "App",
|
"@type": "App",
|
||||||
"initializer": { "@id": "urn:solid-server:default:Initializer" },
|
"initializer": { "@id": "urn:solid-server:default:Initializer" },
|
||||||
"finalizer": {
|
"finalizer": {
|
||||||
"comment": "This is going to contain the list of finalizers that need to be called. These should be added in the configs where such classes are configured.",
|
"comment": "Is executed when the server is stopped.",
|
||||||
"@id": "urn:solid-server:default:Finalizer",
|
"@type": "SequenceHandler",
|
||||||
"@type": "ParallelFinalizer",
|
"handlers": [
|
||||||
"finalizers": [
|
{ "@id": "urn:solid-server:default:Finalizer" },
|
||||||
{ "@id": "urn:solid-server:default:ServerInitializer" }
|
{ "@id": "urn:solid-server:default:CleanupFinalizer" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"clusterManager": {
|
"clusterManager": {
|
||||||
"@id": "urn:solid-server:default:ClusterManager",
|
"@id": "urn:solid-server:default:ClusterManager",
|
||||||
"@type": "ClusterManager",
|
"@type": "ClusterManager",
|
||||||
"workers": { "@id": "urn:solid-server:default:variable:workers" }
|
"workers": { "@id": "urn:solid-server:default:variable:workers" }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "This is going to contain the list of finalizers that need to be called. These should be added in the configs where such classes are configured.",
|
||||||
|
"@id": "urn:solid-server:default:Finalizer",
|
||||||
|
"@type": "ParallelHandler",
|
||||||
|
"handlers": [
|
||||||
|
{
|
||||||
|
"@type": "FinalizableHandler",
|
||||||
|
"finalizable": { "@id": "urn:solid-server:default:ServerInitializer" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "Finalizers that need to cleanup once no more data will be written to the backend should be added here.",
|
||||||
|
"@id": "urn:solid-server:default:CleanupFinalizer",
|
||||||
|
"@type":"SequenceHandler",
|
||||||
|
"handlers": [
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -28,8 +28,13 @@
|
|||||||
{
|
{
|
||||||
"comment": "Makes sure the expiring storage cleanup timer is stopped when the application needs to stop.",
|
"comment": "Makes sure the expiring storage cleanup timer is stopped when the application needs to stop.",
|
||||||
"@id": "urn:solid-server:default:Finalizer",
|
"@id": "urn:solid-server:default:Finalizer",
|
||||||
"@type": "ParallelFinalizer",
|
"@type": "ParallelHandler",
|
||||||
"finalizers": [ { "@id": "urn:solid-server:default:ExpiringForgotPasswordStorage" } ]
|
"handlers": [
|
||||||
|
{
|
||||||
|
"@type": "FinalizableHandler",
|
||||||
|
"finalizable": { "@id": "urn:solid-server:default:ExpiringForgotPasswordStorage" }
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -21,8 +21,13 @@
|
|||||||
{
|
{
|
||||||
"comment": "Makes sure the expiring storage cleanup timer is stopped when the application needs to stop.",
|
"comment": "Makes sure the expiring storage cleanup timer is stopped when the application needs to stop.",
|
||||||
"@id": "urn:solid-server:default:Finalizer",
|
"@id": "urn:solid-server:default:Finalizer",
|
||||||
"@type": "ParallelFinalizer",
|
"@type": "ParallelHandler",
|
||||||
"finalizers": [ { "@id": "urn:solid-server:default:ExpiringTokenStorage" } ]
|
"handlers": [
|
||||||
|
{
|
||||||
|
"@type": "FinalizableHandler",
|
||||||
|
"finalizable": { "@id": "urn:solid-server:default:ExpiringTokenStorage" }
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,13 @@
|
|||||||
{
|
{
|
||||||
"comment": "Makes sure the expiring storage cleanup timer is stopped when the application needs to stop.",
|
"comment": "Makes sure the expiring storage cleanup timer is stopped when the application needs to stop.",
|
||||||
"@id": "urn:solid-server:default:Finalizer",
|
"@id": "urn:solid-server:default:Finalizer",
|
||||||
"@type": "ParallelFinalizer",
|
"@type": "ParallelHandler",
|
||||||
"finalizers": [ { "@id": "urn:solid-server:default:ExpiringAclCache" } ]
|
"handlers": [
|
||||||
|
{
|
||||||
|
"@type": "FinalizableHandler",
|
||||||
|
"finalizable": { "@id": "urn:solid-server:default:ExpiringAclCache" }
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -22,11 +22,25 @@
|
|||||||
"expiration": 3000
|
"expiration": 3000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"comment": "Makes sure the lock folder is cleared and delete when the application needs to stop.",
|
"@id": "urn:solid-server:default:CleanupInitializer",
|
||||||
"@id": "urn:solid-server:default:Finalizer",
|
"@type": "SequenceHandler",
|
||||||
"@type": "ParallelFinalizer",
|
"handlers": [
|
||||||
"finalizers": [
|
{
|
||||||
{ "@id": "urn:solid-server:default:FileSystemResourceLocker" }
|
"comment": "Makes sure the FileSystemResourceLocker starts with a clean slate when the application is started.",
|
||||||
|
"@type": "InitializableHandler",
|
||||||
|
"initializable": { "@id": "urn:solid-server:default:FileSystemResourceLocker" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@id": "urn:solid-server:default:CleanupFinalizer",
|
||||||
|
"@type": "SequenceHandler",
|
||||||
|
"handlers": [
|
||||||
|
{
|
||||||
|
"comment": "Makes sure the lock folder is removed when the application stops.",
|
||||||
|
"@type": "FinalizableHandler",
|
||||||
|
"finalizable": { "@id": "urn:solid-server:default:FileSystemResourceLocker" }
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -12,12 +12,24 @@
|
|||||||
"expiration": 3000
|
"expiration": 3000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"comment": "Makes sure the redis connection is closed when the application needs to stop. Also deletes still-existing locks and counters.",
|
"@id": "urn:solid-server:default:CleanupInitializer",
|
||||||
"@id": "urn:solid-server:default:Finalizer",
|
"@type": "SequenceHandler",
|
||||||
"@type": "ParallelFinalizer",
|
"handlers": [
|
||||||
"finalizers": [
|
|
||||||
{
|
{
|
||||||
"@id": "urn:solid-server:default:RedisLocker"
|
"comment": "Makes sure the RedisLocker starts with a clean slate when the application is started.",
|
||||||
|
"@type": "InitializableHandler",
|
||||||
|
"initializable": { "@id": "urn:solid-server:default:RedisLocker" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@id": "urn:solid-server:default:CleanupFinalizer",
|
||||||
|
"@type": "SequenceHandler",
|
||||||
|
"handlers": [
|
||||||
|
{
|
||||||
|
"comment": "Makes sure the redis connection is closed when the application needs to stop. Also deletes still-existing locks and counters.",
|
||||||
|
"@type": "FinalizableHandler",
|
||||||
|
"finalizable": { "@id": "urn:solid-server:default:RedisLocker" }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,7 @@
|
|||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
"start": "node ./bin/server.js",
|
"start": "node ./bin/server.js",
|
||||||
"start:file": "node ./bin/server.js -c config/file.json -f ./data",
|
"start:file": "node ./bin/server.js -c config/file.json -f ./data",
|
||||||
|
"start:file-no-setup": "node ./bin/server.js -c config/file-no-setup.json -f ./data",
|
||||||
"test": "npm run test:ts && npm run jest",
|
"test": "npm run test:ts && npm run jest",
|
||||||
"test:deploy": "test/deploy/validate-configs.sh",
|
"test:deploy": "test/deploy/validate-configs.sh",
|
||||||
"test:ts": "tsc -p test --noEmit",
|
"test:ts": "tsc -p test --noEmit",
|
||||||
|
@ -194,7 +194,8 @@ export * from './init/cluster/WorkerManager';
|
|||||||
|
|
||||||
// Init/Final
|
// Init/Final
|
||||||
export * from './init/final/Finalizable';
|
export * from './init/final/Finalizable';
|
||||||
export * from './init/final/ParallelFinalizer';
|
export * from './init/final/FinalizableHandler';
|
||||||
|
export * from './init/final/Finalizer';
|
||||||
|
|
||||||
// Init/Setup
|
// Init/Setup
|
||||||
export * from './init/setup/SetupHandler';
|
export * from './init/setup/SetupHandler';
|
||||||
@ -221,6 +222,8 @@ export * from './init/BaseUrlVerifier';
|
|||||||
export * from './init/CliResolver';
|
export * from './init/CliResolver';
|
||||||
export * from './init/ConfigPodInitializer';
|
export * from './init/ConfigPodInitializer';
|
||||||
export * from './init/ContainerInitializer';
|
export * from './init/ContainerInitializer';
|
||||||
|
export * from './init/Initializable';
|
||||||
|
export * from './init/InitializableHandler';
|
||||||
export * from './init/Initializer';
|
export * from './init/Initializer';
|
||||||
export * from './init/LoggerInitializer';
|
export * from './init/LoggerInitializer';
|
||||||
export * from './init/ModuleVersionVerifier';
|
export * from './init/ModuleVersionVerifier';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { ClusterManager } from './cluster/ClusterManager';
|
import type { ClusterManager } from './cluster/ClusterManager';
|
||||||
import type { Finalizable } from './final/Finalizable';
|
import type { Finalizer } from './final/Finalizer';
|
||||||
import type { Initializer } from './Initializer';
|
import type { Initializer } from './Initializer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -7,10 +7,10 @@ import type { Initializer } from './Initializer';
|
|||||||
*/
|
*/
|
||||||
export class App {
|
export class App {
|
||||||
private readonly initializer: Initializer;
|
private readonly initializer: Initializer;
|
||||||
private readonly finalizer: Finalizable;
|
private readonly finalizer: Finalizer;
|
||||||
public readonly clusterManager: ClusterManager;
|
public readonly clusterManager: ClusterManager;
|
||||||
|
|
||||||
public constructor(initializer: Initializer, finalizer: Finalizable, clusterManager: ClusterManager) {
|
public constructor(initializer: Initializer, finalizer: Finalizer, clusterManager: ClusterManager) {
|
||||||
this.initializer = initializer;
|
this.initializer = initializer;
|
||||||
this.finalizer = finalizer;
|
this.finalizer = finalizer;
|
||||||
this.clusterManager = clusterManager;
|
this.clusterManager = clusterManager;
|
||||||
@ -27,6 +27,6 @@ export class App {
|
|||||||
* Stops the application and handles cleanup.
|
* Stops the application and handles cleanup.
|
||||||
*/
|
*/
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
await this.finalizer.finalize();
|
await this.finalizer.handleSafe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8
src/init/Initializable.ts
Normal file
8
src/init/Initializable.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Allows for initializing state or executing logic when the application is started.
|
||||||
|
* Use this interface to add initialization logic to classes that already extend some other type.
|
||||||
|
* NOTE: classes without an existing extends-relation should extend from Initializer instead!
|
||||||
|
*/
|
||||||
|
export interface Initializable {
|
||||||
|
initialize: () => Promise<void>;
|
||||||
|
}
|
18
src/init/InitializableHandler.ts
Normal file
18
src/init/InitializableHandler.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { Initializable } from './Initializable';
|
||||||
|
import { Initializer } from './Initializer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows using an Initializable as an Initializer Handler.
|
||||||
|
*/
|
||||||
|
export class InitializableHandler extends Initializer {
|
||||||
|
protected readonly initializable: Initializable;
|
||||||
|
|
||||||
|
public constructor(initializable: Initializable) {
|
||||||
|
super();
|
||||||
|
this.initializable = initializable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handle(): Promise<void> {
|
||||||
|
return this.initializable.initialize();
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
import { AsyncHandler } from '../util/handlers/AsyncHandler';
|
import { AsyncHandler } from '../util/handlers/AsyncHandler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializer is used to indicate an AsyncHandler that performs initialization logic.
|
||||||
|
*/
|
||||||
export abstract class Initializer extends AsyncHandler {}
|
export abstract class Initializer extends AsyncHandler {}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Allows for cleaning up an object and stopping relevant loops when the application needs to be stopped.
|
* Allows for cleaning up an object and stopping relevant loops when the application needs to be stopped.
|
||||||
|
* Use this interface to add finalization logic to classes that already extend some other type.
|
||||||
|
* NOTE: classes without an existing extends-relation should extend from Finalizer instead!
|
||||||
*/
|
*/
|
||||||
export interface Finalizable {
|
export interface Finalizable {
|
||||||
finalize: () => Promise<void>;
|
finalize: () => Promise<void>;
|
||||||
|
18
src/init/final/FinalizableHandler.ts
Normal file
18
src/init/final/FinalizableHandler.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { Finalizable } from './Finalizable';
|
||||||
|
import { Finalizer } from './Finalizer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows using a Finalizable as a Finalizer Handler.
|
||||||
|
*/
|
||||||
|
export class FinalizableHandler extends Finalizer {
|
||||||
|
protected readonly finalizable: Finalizable;
|
||||||
|
|
||||||
|
public constructor(finalizable: Finalizable) {
|
||||||
|
super();
|
||||||
|
this.finalizable = finalizable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handle(): Promise<void> {
|
||||||
|
return this.finalizable.finalize();
|
||||||
|
}
|
||||||
|
}
|
6
src/init/final/Finalizer.ts
Normal file
6
src/init/final/Finalizer.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finalizer is used to indicate an AsyncHandler that performs finalization logic.
|
||||||
|
*/
|
||||||
|
export abstract class Finalizer extends AsyncHandler {}
|
@ -1,16 +0,0 @@
|
|||||||
import type { Finalizable } from './Finalizable';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finalizes all the injected Finalizable classes in parallel.
|
|
||||||
*/
|
|
||||||
export class ParallelFinalizer implements Finalizable {
|
|
||||||
private readonly finalizers: Finalizable[];
|
|
||||||
|
|
||||||
public constructor(finalizers: Finalizable[] = []) {
|
|
||||||
this.finalizers = finalizers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async finalize(): Promise<void> {
|
|
||||||
await Promise.all(this.finalizers.map(async(finalizer): Promise<void> => finalizer.finalize()));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +1,10 @@
|
|||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { ensureDirSync, pathExists, readdir, rmdir } from 'fs-extra';
|
import { ensureDir, remove } from 'fs-extra';
|
||||||
import type { LockOptions, UnlockOptions } from 'proper-lockfile';
|
import type { LockOptions, UnlockOptions } from 'proper-lockfile';
|
||||||
import { lock, unlock } from 'proper-lockfile';
|
import { lock, unlock } from 'proper-lockfile';
|
||||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||||
import type { Finalizable } from '../../init/final/Finalizable';
|
import type { Finalizable } from '../../init/final/Finalizable';
|
||||||
|
import type { Initializable } from '../../init/Initializable';
|
||||||
import { getLoggerFor } from '../../logging/LogUtil';
|
import { getLoggerFor } from '../../logging/LogUtil';
|
||||||
import { createErrorMessage } from '../errors/ErrorUtil';
|
import { createErrorMessage } from '../errors/ErrorUtil';
|
||||||
import { InternalServerError } from '../errors/InternalServerError';
|
import { InternalServerError } from '../errors/InternalServerError';
|
||||||
@ -34,9 +35,12 @@ const attemptDefaults: Required<AttemptSettings> = { retryCount: -1, retryDelay:
|
|||||||
* Argument interface of the FileSystemResourceLocker constructor.
|
* Argument interface of the FileSystemResourceLocker constructor.
|
||||||
*/
|
*/
|
||||||
interface FileSystemResourceLockerArgs {
|
interface FileSystemResourceLockerArgs {
|
||||||
/** The rootPath of the filesystem */
|
/** The rootPath of the filesystem _[default is the current dir `./`]_ */
|
||||||
rootFilePath?: string;
|
rootFilePath?: string;
|
||||||
/** The path to the directory where locks will be stored (appended to rootFilePath) */
|
/**
|
||||||
|
* The path to the directory where locks will be stored (appended to rootFilePath)
|
||||||
|
* _[default is `/.internal/locks`]_
|
||||||
|
*/
|
||||||
lockDirectory?: string;
|
lockDirectory?: string;
|
||||||
/** Custom settings concerning retrying locks */
|
/** Custom settings concerning retrying locks */
|
||||||
attemptSettings?: AttemptSettings;
|
attemptSettings?: AttemptSettings;
|
||||||
@ -54,24 +58,22 @@ function isCodedError(err: unknown): err is { code: string } & Error {
|
|||||||
* either resolve successfully or reject immediately with the causing error. The retry function of the library
|
* either resolve successfully or reject immediately with the causing error. The retry function of the library
|
||||||
* however will be ignored and replaced by our own LockUtils' {@link retryFunctionUntil} function.
|
* however will be ignored and replaced by our own LockUtils' {@link retryFunctionUntil} function.
|
||||||
*/
|
*/
|
||||||
export class FileSystemResourceLocker implements ResourceLocker, Finalizable {
|
export class FileSystemResourceLocker implements ResourceLocker, Initializable, Finalizable {
|
||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
private readonly attemptSettings: Required<AttemptSettings>;
|
private readonly attemptSettings: Required<AttemptSettings>;
|
||||||
/** Folder that stores the locks */
|
/** Folder that stores the locks */
|
||||||
private readonly lockFolder: string;
|
private readonly lockFolder: string;
|
||||||
|
private finalized = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new FileSystemResourceLocker
|
* Create a new FileSystemResourceLocker
|
||||||
* @param rootFilePath - The rootPath of the filesystem _[default is the current dir `./`]_
|
* @param args - Configures the locker using the specified FileSystemResourceLockerArgs instance.
|
||||||
* @param lockDirectory - The path to the directory where locks will be stored (appended to rootFilePath)
|
|
||||||
_[default is `/.internal/locks`]_
|
|
||||||
* @param attemptSettings - Custom settings concerning retrying locks
|
|
||||||
*/
|
*/
|
||||||
public constructor(args: FileSystemResourceLockerArgs = {}) {
|
public constructor(args: FileSystemResourceLockerArgs = {}) {
|
||||||
const { rootFilePath, lockDirectory, attemptSettings } = args;
|
const { rootFilePath, lockDirectory, attemptSettings } = args;
|
||||||
|
defaultLockOptions.onCompromised = this.customOnCompromised.bind(this);
|
||||||
this.attemptSettings = { ...attemptDefaults, ...attemptSettings };
|
this.attemptSettings = { ...attemptDefaults, ...attemptSettings };
|
||||||
this.lockFolder = joinFilePath(rootFilePath ?? './', lockDirectory ?? '/.internal/locks');
|
this.lockFolder = joinFilePath(rootFilePath ?? './', lockDirectory ?? '/.internal/locks');
|
||||||
ensureDirSync(this.lockFolder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -149,13 +151,36 @@ export class FileSystemResourceLocker implements ResourceLocker, Finalizable {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializer method to be executed on server start. This makes sure that no pre-existing (dangling) locks
|
||||||
|
* remain on disk, so that request will not be blocked because a lock was acquired in the previous server instance.
|
||||||
|
*
|
||||||
|
* NOTE: this also removes locks created by the GreedyReadWriteLocker.
|
||||||
|
* (See issue: https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1358)
|
||||||
|
*/
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
// Remove all existing (dangling) locks so new requests are not blocked (by removing the lock folder).
|
||||||
|
await remove(this.lockFolder);
|
||||||
|
// Put the folder back since `proper-lockfile` depends on its existence.
|
||||||
|
return ensureDir(this.lockFolder);
|
||||||
|
}
|
||||||
|
|
||||||
public async finalize(): Promise<void> {
|
public async finalize(): Promise<void> {
|
||||||
// Delete lingering locks in the lockFolder.
|
// Register that finalize was called by setting a state variable.
|
||||||
if (await pathExists(this.lockFolder)) {
|
this.finalized = true;
|
||||||
for (const dir of await readdir(this.lockFolder)) {
|
// NOTE: in contrast with initialize(), the lock folder is not cleared here, as the proper-lock library
|
||||||
await rmdir(joinFilePath(this.lockFolder, dir));
|
// manages these files and will attempt to clear existing files when the process is shutdown gracefully.
|
||||||
}
|
}
|
||||||
await rmdir(this.lockFolder);
|
|
||||||
|
/**
|
||||||
|
* This function is used to override the proper-lock onCompromised function.
|
||||||
|
* Once the locker was finalized, it will log the provided error instead of throwing it
|
||||||
|
* This allows for a clean shutdown procedure.
|
||||||
|
*/
|
||||||
|
private customOnCompromised(err: any): void {
|
||||||
|
if (!this.finalized) {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
this.logger.warn(`onCompromised was called with error: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||||
import type { Finalizable } from '../../init/final/Finalizable';
|
import type { Finalizable } from '../../init/final/Finalizable';
|
||||||
|
import type { Initializable } from '../../init/Initializable';
|
||||||
import { getLoggerFor } from '../../logging/LogUtil';
|
import { getLoggerFor } from '../../logging/LogUtil';
|
||||||
import type { AttemptSettings } from '../LockUtils';
|
import type { AttemptSettings } from '../LockUtils';
|
||||||
import { retryFunction } from '../LockUtils';
|
import { retryFunction } from '../LockUtils';
|
||||||
@ -42,13 +43,14 @@ const PREFIX_LOCK = '__L__';
|
|||||||
* * @see [Redis Lua scripting documentation](https://redis.io/docs/manual/programmability/)
|
* * @see [Redis Lua scripting documentation](https://redis.io/docs/manual/programmability/)
|
||||||
* * @see [ioredis Lua scripting API](https://github.com/luin/ioredis#lua-scripting)
|
* * @see [ioredis Lua scripting API](https://github.com/luin/ioredis#lua-scripting)
|
||||||
*/
|
*/
|
||||||
export class RedisLocker implements ReadWriteLocker, ResourceLocker, Finalizable {
|
export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializable, Finalizable {
|
||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
private readonly redis: Redis;
|
private readonly redis: Redis;
|
||||||
private readonly redisRw: RedisReadWriteLock;
|
private readonly redisRw: RedisReadWriteLock;
|
||||||
private readonly redisLock: RedisResourceLock;
|
private readonly redisLock: RedisResourceLock;
|
||||||
private readonly attemptSettings: Required<AttemptSettings>;
|
private readonly attemptSettings: Required<AttemptSettings>;
|
||||||
|
private finalized = false;
|
||||||
|
|
||||||
public constructor(redisClient = '127.0.0.1:6379', attemptSettings: AttemptSettings = {}) {
|
public constructor(redisClient = '127.0.0.1:6379', attemptSettings: AttemptSettings = {}) {
|
||||||
this.redis = this.createRedisClient(redisClient);
|
this.redis = this.createRedisClient(redisClient);
|
||||||
@ -113,6 +115,9 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Finalizable
|
|||||||
* @param fn - The function reference to swallow false from.
|
* @param fn - The function reference to swallow false from.
|
||||||
*/
|
*/
|
||||||
private swallowFalse(fn: () => Promise<RedisAnswer>): () => Promise<unknown> {
|
private swallowFalse(fn: () => Promise<RedisAnswer>): () => Promise<unknown> {
|
||||||
|
if (this.finalized) {
|
||||||
|
throw new Error('Invalid state: cannot execute Redis operation once finalize() has been called.');
|
||||||
|
}
|
||||||
return async(): Promise<unknown> => {
|
return async(): Promise<unknown> => {
|
||||||
const result = await fromResp2ToBool(fn());
|
const result = await fromResp2ToBool(fn());
|
||||||
// Swallow any result resolving to `false`
|
// Swallow any result resolving to `false`
|
||||||
@ -172,24 +177,36 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Finalizable
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Finalizer methods */
|
/* Initializer & Finalizer methods */
|
||||||
|
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
// On server start: remove all existing (dangling) locks, so new requests are not blocked.
|
||||||
|
return this.clearLocks();
|
||||||
|
}
|
||||||
|
|
||||||
public async finalize(): Promise<void> {
|
public async finalize(): Promise<void> {
|
||||||
// This for loop is an extra failsafe,
|
this.finalized = true;
|
||||||
// this extra code won't slow down anything, this function will only be called to shut down in peace
|
|
||||||
try {
|
try {
|
||||||
// Remove any lock still open, since once closed, they should no longer be held.
|
// On controlled server shutdown: clean up all existing locks.
|
||||||
const keysRw = await this.redisRw.keys(`${PREFIX_RW}*`);
|
return await this.clearLocks();
|
||||||
if (keysRw.length > 0) {
|
|
||||||
await this.redisRw.del(...keysRw);
|
|
||||||
}
|
|
||||||
|
|
||||||
const keysLock = await this.redisLock.keys(`${PREFIX_LOCK}*`);
|
|
||||||
if (keysLock.length > 0) {
|
|
||||||
await this.redisLock.del(...keysLock);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
|
// Always quit the redis client
|
||||||
await this.redis.quit();
|
await this.redis.quit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove any lock still open
|
||||||
|
*/
|
||||||
|
private async clearLocks(): Promise<void> {
|
||||||
|
const keysRw = await this.redisRw.keys(`${PREFIX_RW}*`);
|
||||||
|
if (keysRw.length > 0) {
|
||||||
|
await this.redisRw.del(...keysRw);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keysLock = await this.redisLock.keys(`${PREFIX_LOCK}*`);
|
||||||
|
if (keysLock.length > 0) {
|
||||||
|
await this.redisLock.del(...keysLock);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from '.
|
|||||||
/**
|
/**
|
||||||
* Test the general functionality of the server using a RedisLocker with Read-Write strategy.
|
* Test the general functionality of the server using a RedisLocker with Read-Write strategy.
|
||||||
*/
|
*/
|
||||||
describeIf('docker', 'A server with a RedisLocker', (): void => {
|
describeIf('docker')('A server with a RedisLocker', (): void => {
|
||||||
const port = getPort('RedisLocker');
|
const port = getPort('RedisLocker');
|
||||||
const baseUrl = `http://localhost:${port}/`;
|
const baseUrl = `http://localhost:${port}/`;
|
||||||
let app: App;
|
let app: App;
|
||||||
|
84
test/integration/ResourceLockCleanup.test.ts
Normal file
84
test/integration/ResourceLockCleanup.test.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import fetch from 'cross-fetch';
|
||||||
|
import type { App, DataAccessorBasedStore, Initializable, ResourceLocker } from '../../src';
|
||||||
|
import { readableToString, BasicRepresentation } from '../../src';
|
||||||
|
import { describeIf, getPort } from '../util/Util';
|
||||||
|
import { getDefaultVariables, getTestConfigPath, getTestFolder, instantiateFromConfig, removeFolder } from './Config';
|
||||||
|
|
||||||
|
const port = getPort('ResourceLockCleanup');
|
||||||
|
const baseUrl = `http://localhost:${port}/`;
|
||||||
|
const rootFilePath = getTestFolder(`resource-lock-cleanup`);
|
||||||
|
const resourceIdentifier = { path: `${baseUrl}container1/test.txt` };
|
||||||
|
|
||||||
|
const configs: [string, any][] = [
|
||||||
|
[
|
||||||
|
'file-based', {
|
||||||
|
config: 'server-file.json',
|
||||||
|
init: async(initializable: Initializable): Promise<void> => initializable.initialize(),
|
||||||
|
teardown: async(): Promise<void> => removeFolder(rootFilePath),
|
||||||
|
}],
|
||||||
|
[
|
||||||
|
'redis-based', {
|
||||||
|
config: 'server-redis-lock.json',
|
||||||
|
init: jest.fn(),
|
||||||
|
teardown: jest.fn(),
|
||||||
|
}],
|
||||||
|
];
|
||||||
|
|
||||||
|
describeIf('docker').each(configs)('A server using %s locking', (id, { config, init, teardown }):
|
||||||
|
void => {
|
||||||
|
let app: App;
|
||||||
|
let store: DataAccessorBasedStore;
|
||||||
|
let locker: ResourceLocker;
|
||||||
|
|
||||||
|
beforeAll(async(): Promise<void> => {
|
||||||
|
const variables = {
|
||||||
|
...getDefaultVariables(port, baseUrl),
|
||||||
|
'urn:solid-server:default:variable:rootFilePath': rootFilePath,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the server
|
||||||
|
const instances = await instantiateFromConfig(
|
||||||
|
'urn:solid-server:test:Instances',
|
||||||
|
[
|
||||||
|
getTestConfigPath(config),
|
||||||
|
],
|
||||||
|
variables,
|
||||||
|
) as Record<string, any>;
|
||||||
|
({ app, store, locker } = instances);
|
||||||
|
// Create the test resource
|
||||||
|
await store.setRepresentation(resourceIdentifier, new BasicRepresentation('abc', 'text/plain'));
|
||||||
|
|
||||||
|
// Perform additional initialization, if configured
|
||||||
|
await init(locker);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async(): Promise<void> => {
|
||||||
|
// Stop the server
|
||||||
|
await app.stop();
|
||||||
|
|
||||||
|
// Execute the configured teardown
|
||||||
|
await teardown();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be affected by dangling locks.', async(): Promise<void> => {
|
||||||
|
// Simulate lock existing before server startup, by creating a (write) lock directly
|
||||||
|
await locker.acquire({ path: `${resourceIdentifier.path}.write` });
|
||||||
|
// Start the server
|
||||||
|
await app.start();
|
||||||
|
|
||||||
|
// Updating the resource should succeed (if the server clears dangling locks on startup).
|
||||||
|
const updatedContent = 'def';
|
||||||
|
const result = await fetch(resourceIdentifier.path, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'text/plain',
|
||||||
|
},
|
||||||
|
body: updatedContent,
|
||||||
|
});
|
||||||
|
expect(result.status).toBe(205);
|
||||||
|
// Check if the resource was updated:
|
||||||
|
const representation = await store.getRepresentation(resourceIdentifier);
|
||||||
|
const data = await readableToString(representation.data);
|
||||||
|
expect(data).toEqual(updatedContent);
|
||||||
|
});
|
||||||
|
});
|
@ -8,7 +8,7 @@ import { getDefaultVariables, getPresetConfigPath, getTestConfigPath, instantiat
|
|||||||
const port = getPort('SparqlStorage');
|
const port = getPort('SparqlStorage');
|
||||||
const baseUrl = `http://localhost:${port}/`;
|
const baseUrl = `http://localhost:${port}/`;
|
||||||
|
|
||||||
describeIf('docker', 'A server with a SPARQL endpoint as storage', (): void => {
|
describeIf('docker')('A server with a SPARQL endpoint as storage', (): void => {
|
||||||
let app: App;
|
let app: App;
|
||||||
|
|
||||||
beforeAll(async(): Promise<void> => {
|
beforeAll(async(): Promise<void> => {
|
||||||
|
@ -27,17 +27,25 @@
|
|||||||
"css:config/util/index/default.json",
|
"css:config/util/index/default.json",
|
||||||
"css:config/util/logging/winston.json",
|
"css:config/util/logging/winston.json",
|
||||||
"css:config/util/representation-conversion/default.json",
|
"css:config/util/representation-conversion/default.json",
|
||||||
"css:config/util/resource-locker/memory.json",
|
"css:config/util/resource-locker/file.json",
|
||||||
"css:config/util/variables/default.json"
|
"css:config/util/variables/default.json"
|
||||||
],
|
],
|
||||||
"@graph": [
|
"@graph": [
|
||||||
{
|
{
|
||||||
"@id": "urn:solid-server:test:Instances",
|
"@id": "urn:solid-server:test:Instances",
|
||||||
"@type": "RecordObject",
|
"@type": "RecordObject",
|
||||||
"RecordObject:_record": [
|
"record": [
|
||||||
{
|
{
|
||||||
"RecordObject:_record_key": "app",
|
"RecordObject:_record_key": "app",
|
||||||
"RecordObject:_record_value": { "@id": "urn:solid-server:default:App" }
|
"RecordObject:_record_value": { "@id": "urn:solid-server:default:App" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"RecordObject:_record_key": "store",
|
||||||
|
"RecordObject:_record_value": { "@id": "urn:solid-server:default:ResourceStore_Backend" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"RecordObject:_record_key": "locker",
|
||||||
|
"RecordObject:_record_value": { "@id": "urn:solid-server:default:FileSystemResourceLocker" }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -38,6 +38,10 @@
|
|||||||
"RecordObject:_record_key": "app",
|
"RecordObject:_record_key": "app",
|
||||||
"RecordObject:_record_value": { "@id": "urn:solid-server:default:App" }
|
"RecordObject:_record_value": { "@id": "urn:solid-server:default:App" }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"RecordObject:_record_key": "store",
|
||||||
|
"RecordObject:_record_value": { "@id": "urn:solid-server:default:ResourceStore_Backend" }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"RecordObject:_record_key": "locker",
|
"RecordObject:_record_key": "locker",
|
||||||
"RecordObject:_record_value": { "@id": "urn:solid-server:default:RedisLocker" }
|
"RecordObject:_record_value": { "@id": "urn:solid-server:default:RedisLocker" }
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import type { ClusterManager } from '../../../src';
|
import type { ClusterManager } from '../../../src';
|
||||||
import { App } from '../../../src/init/App';
|
import { App } from '../../../src/init/App';
|
||||||
import type { Finalizable } from '../../../src/init/final/Finalizable';
|
import type { Finalizer } from '../../../src/init/final/Finalizer';
|
||||||
import type { Initializer } from '../../../src/init/Initializer';
|
import type { Initializer } from '../../../src/init/Initializer';
|
||||||
|
|
||||||
describe('An App', (): void => {
|
describe('An App', (): void => {
|
||||||
let initializer: Initializer;
|
let initializer: Initializer;
|
||||||
let finalizer: Finalizable;
|
let finalizer: Finalizer;
|
||||||
let clusterManager: ClusterManager;
|
let clusterManager: ClusterManager;
|
||||||
let app: App;
|
let app: App;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
initializer = { handleSafe: jest.fn() } as any;
|
initializer = { handleSafe: jest.fn() } as any;
|
||||||
finalizer = { finalize: jest.fn() };
|
finalizer = { handleSafe: jest.fn() } as any;
|
||||||
clusterManager = {} as any;
|
clusterManager = {} as any;
|
||||||
app = new App(initializer, finalizer, clusterManager);
|
app = new App(initializer, finalizer, clusterManager);
|
||||||
});
|
});
|
||||||
@ -23,7 +23,7 @@ describe('An App', (): void => {
|
|||||||
|
|
||||||
it('can stop with the finalizer.', async(): Promise<void> => {
|
it('can stop with the finalizer.', async(): Promise<void> => {
|
||||||
await expect(app.stop()).resolves.toBeUndefined();
|
await expect(app.stop()).resolves.toBeUndefined();
|
||||||
expect(finalizer.finalize).toHaveBeenCalledTimes(1);
|
expect(finalizer.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can check its clusterManager for the threading mode.', async(): Promise<void> => {
|
it('can check its clusterManager for the threading mode.', async(): Promise<void> => {
|
||||||
|
11
test/unit/init/InitializableHandler.test.ts
Normal file
11
test/unit/init/InitializableHandler.test.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { InitializableHandler } from '../../../src';
|
||||||
|
|
||||||
|
describe('InitializableHandler', (): void => {
|
||||||
|
const initializable = { initialize: jest.fn() };
|
||||||
|
const initializer = new InitializableHandler(initializable);
|
||||||
|
|
||||||
|
it('redirects handle towards initialize.', async(): Promise<void> => {
|
||||||
|
await initializer.handleSafe();
|
||||||
|
expect(initializable.initialize).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
11
test/unit/init/final/FinalizableHandler.test.ts
Normal file
11
test/unit/init/final/FinalizableHandler.test.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { FinalizableHandler } from '../../../../src';
|
||||||
|
|
||||||
|
describe('FinalizableHandler', (): void => {
|
||||||
|
const finalizable = { finalize: jest.fn() };
|
||||||
|
const finalizer = new FinalizableHandler(finalizable);
|
||||||
|
|
||||||
|
it('redirects handle towards finalize.', async(): Promise<void> => {
|
||||||
|
await finalizer.handleSafe();
|
||||||
|
expect(finalizable.finalize).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
@ -1,30 +0,0 @@
|
|||||||
import type { Finalizable } from '../../../../src/init/final/Finalizable';
|
|
||||||
import { ParallelFinalizer } from '../../../../src/init/final/ParallelFinalizer';
|
|
||||||
|
|
||||||
describe('A ParallelFinalizer', (): void => {
|
|
||||||
let finalizers: Finalizable[];
|
|
||||||
let finalizer: ParallelFinalizer;
|
|
||||||
let results: number[];
|
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
|
||||||
results = [];
|
|
||||||
finalizers = [
|
|
||||||
{ finalize: jest.fn((): any => results.push(0)) },
|
|
||||||
{ finalize: jest.fn((): any => results.push(1)) },
|
|
||||||
];
|
|
||||||
|
|
||||||
finalizer = new ParallelFinalizer(finalizers);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is finished when all finalizers are finished.', async(): Promise<void> => {
|
|
||||||
await expect(finalizer.finalize()).resolves.toBeUndefined();
|
|
||||||
expect(finalizers[0].finalize).toHaveBeenCalledTimes(1);
|
|
||||||
expect(finalizers[1].finalize).toHaveBeenCalledTimes(1);
|
|
||||||
expect(results).toEqual([ 0, 1 ]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('works if there are no input finalizers.', async(): Promise<void> => {
|
|
||||||
finalizer = new ParallelFinalizer();
|
|
||||||
await expect(finalizer.finalize()).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,6 +1,5 @@
|
|||||||
import { readdir } from 'fs-extra';
|
import { readdir } from 'fs-extra';
|
||||||
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
|
import { InternalServerError, FileSystemResourceLocker } from '../../../../src';
|
||||||
import { FileSystemResourceLocker } from '../../../../src/util/locking/FileSystemResourceLocker';
|
|
||||||
|
|
||||||
const lockFolder = './.internal/locks/';
|
const lockFolder = './.internal/locks/';
|
||||||
|
|
||||||
@ -10,6 +9,7 @@ describe('A FileSystemResourceLocker', (): void => {
|
|||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
locker = new FileSystemResourceLocker({ attemptSettings: { retryCount: 19, retryDelay: 100 }});
|
locker = new FileSystemResourceLocker({ attemptSettings: { retryCount: 19, retryDelay: 100 }});
|
||||||
|
await locker.initialize();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async(): Promise<void> => {
|
afterEach(async(): Promise<void> => {
|
||||||
@ -109,13 +109,20 @@ describe('A FileSystemResourceLocker', (): void => {
|
|||||||
await expect(locker.acquire(identifier)).rejects.toThrow(InternalServerError);
|
await expect(locker.acquire(identifier)).rejects.toThrow(InternalServerError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clears the files in de lock directory after calling finalize.', async(): Promise<void> => {
|
it('clears the files in de lock directory upon calling initialize.', async(): Promise<void> => {
|
||||||
await locker.acquire(identifier);
|
await locker.acquire(identifier);
|
||||||
await expect(readdir(lockFolder)).resolves.toHaveLength(1);
|
await expect(readdir(lockFolder)).resolves.toHaveLength(1);
|
||||||
await locker.finalize();
|
await locker.initialize();
|
||||||
await expect(readdir(lockFolder)).rejects.toThrow();
|
await expect(readdir(lockFolder)).resolves.toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('stops proper-lock from throwing errors onCompromise after finalize was called.',
|
||||||
|
async(): Promise<void> => {
|
||||||
|
expect((): void => (locker as any).customOnCompromised(new Error('test'))).toThrow();
|
||||||
|
await locker.finalize();
|
||||||
|
expect((locker as any).customOnCompromised(new Error('test'))).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('can create a locker with default AttemptSettings.', async(): Promise<void> => {
|
it('can create a locker with default AttemptSettings.', async(): Promise<void> => {
|
||||||
expect((): FileSystemResourceLocker => new FileSystemResourceLocker()).not.toThrow();
|
expect((): FileSystemResourceLocker => new FileSystemResourceLocker()).not.toThrow();
|
||||||
});
|
});
|
||||||
|
@ -70,8 +70,20 @@ const store = {
|
|||||||
const redis: jest.Mocked<Redis & RedisResourceLock & RedisReadWriteLock> = {
|
const redis: jest.Mocked<Redis & RedisResourceLock & RedisReadWriteLock> = {
|
||||||
defineCommand: jest.fn(),
|
defineCommand: jest.fn(),
|
||||||
quit: jest.fn(),
|
quit: jest.fn(),
|
||||||
keys: jest.fn().mockResolvedValue([]),
|
keys: jest.fn().mockImplementation(async(pattern: string): Promise<string[]> =>
|
||||||
del: jest.fn(),
|
Object.keys(store.internal)
|
||||||
|
.filter((value: string): boolean => new RegExp(pattern, 'u').test(value))),
|
||||||
|
del: jest.fn().mockImplementation(async(...keys: string[]): Promise<number> => {
|
||||||
|
let deletedEntries = 0;
|
||||||
|
for (const key of keys) {
|
||||||
|
if (typeof store.internal[key] !== 'undefined') {
|
||||||
|
deletedEntries += 1;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
|
delete store.internal[key];
|
||||||
|
}
|
||||||
|
return deletedEntries;
|
||||||
|
}),
|
||||||
acquireReadLock: jest.fn().mockImplementation(async(key: string): Promise<number> =>
|
acquireReadLock: jest.fn().mockImplementation(async(key: string): Promise<number> =>
|
||||||
store.acquireReadLock(key)),
|
store.acquireReadLock(key)),
|
||||||
acquireWriteLock: jest.fn().mockImplementation(async(key: string): Promise<number | null | 'OK'> =>
|
acquireWriteLock: jest.fn().mockImplementation(async(key: string): Promise<number | null | 'OK'> =>
|
||||||
@ -407,29 +419,14 @@ describe('A RedisLocker', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('finalize()', (): void => {
|
describe('finalize()', (): void => {
|
||||||
it('should quit when there are no more keys when finalize() is called.', async(): Promise<void> => {
|
it('should call quit and clear Read-Write locks when finalize() is called.', async(): Promise<void> => {
|
||||||
// This works since the Redis is simply a mock and quit should have cleared the internal store
|
const promise = locker.withWriteLock(resource1, async(): Promise<any> => {
|
||||||
await locker.withWriteLock(resource1, async(): Promise<any> => {
|
|
||||||
await locker.finalize();
|
|
||||||
expect(redis.quit).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should clear all lock keys when finalize() is called.', async(): Promise<void> => {
|
|
||||||
redis.keys.mockResolvedValueOnce([ '__L__k1', '__L__k2' ]);
|
|
||||||
// This works since the Redis is simply a mock and quit should have cleared the internal store
|
|
||||||
await locker.withWriteLock(resource1, async(): Promise<any> => {
|
|
||||||
await locker.finalize();
|
|
||||||
expect(redis.quit).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clear all rw keys when finalize() is called.', async(): Promise<void> => {
|
|
||||||
redis.keys.mockResolvedValueOnce([ '__RW__k1', '__RW__k2' ]);
|
|
||||||
// This works since the Redis is simply a mock and quit should have cleared the internal store
|
|
||||||
await locker.withWriteLock(resource1, async(): Promise<any> => {
|
|
||||||
await locker.finalize();
|
await locker.finalize();
|
||||||
|
expect(Object.keys(store.internal)).toHaveLength(0);
|
||||||
expect(redis.quit).toHaveBeenCalledTimes(1);
|
expect(redis.quit).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
// Auto-release of Read-Write lock should result in an exception, as the Locker has been finalized.
|
||||||
|
await expect(promise).rejects.toThrow(/Invalid state/u);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -521,17 +518,27 @@ describe('A RedisLocker', (): void => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('initialize()', (): void => {
|
||||||
|
it('should clear all locks when initialize() is called.', async(): Promise<void> => {
|
||||||
|
await locker.acquire({ path: 'path1' });
|
||||||
|
await locker.acquire({ path: 'path2' });
|
||||||
|
await locker.initialize();
|
||||||
|
expect(Object.keys(store.internal)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('finalize()', (): void => {
|
describe('finalize()', (): void => {
|
||||||
it('should clear all locks (even when empty) when finalize() is called.', async(): Promise<void> => {
|
it('should clear all locks (even when empty) when finalize() is called.', async(): Promise<void> => {
|
||||||
await locker.finalize();
|
await locker.finalize();
|
||||||
|
expect(Object.keys(store.internal)).toHaveLength(0);
|
||||||
expect(redis.quit).toHaveBeenCalledTimes(1);
|
expect(redis.quit).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clear all locks when finalize() is called.', async(): Promise<void> => {
|
it('should clear all locks when finalize() is called.', async(): Promise<void> => {
|
||||||
redis.keys
|
await locker.acquire({ path: 'path1' });
|
||||||
.mockResolvedValueOnce([ '__L__k1', '__L__k2' ])
|
await locker.acquire({ path: 'path2' });
|
||||||
.mockResolvedValueOnce([ '__L__k1', '__L__k2' ]);
|
|
||||||
await locker.finalize();
|
await locker.finalize();
|
||||||
|
expect(Object.keys(store.internal)).toHaveLength(0);
|
||||||
expect(redis.quit).toHaveBeenCalledTimes(1);
|
expect(redis.quit).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { Dirent, Stats } from 'fs';
|
import type { Dirent, Stats } from 'fs';
|
||||||
import { PassThrough, Readable } from 'stream';
|
import { PassThrough, Readable } from 'stream';
|
||||||
import type { SystemError } from '../../src/util/errors/SystemError';
|
import type { SystemError } from '../../src/util/errors/SystemError';
|
||||||
|
import Describe = jest.Describe;
|
||||||
|
|
||||||
const portNames = [
|
const portNames = [
|
||||||
// Integration
|
// Integration
|
||||||
@ -19,6 +20,7 @@ const portNames = [
|
|||||||
'PodCreation',
|
'PodCreation',
|
||||||
'PodQuota',
|
'PodQuota',
|
||||||
'RedisLocker',
|
'RedisLocker',
|
||||||
|
'ResourceLockCleanup',
|
||||||
'RestrictedIdentity',
|
'RestrictedIdentity',
|
||||||
'SeedingPods',
|
'SeedingPods',
|
||||||
'ServerFetch',
|
'ServerFetch',
|
||||||
@ -40,11 +42,10 @@ export function getPort(name: typeof portNames[number]): number {
|
|||||||
return 6000 + idx;
|
return 6000 + idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function describeIf(envFlag: string, name: string, fn: () => void): void {
|
export function describeIf(envFlag: string): Describe {
|
||||||
const flag = `TEST_${envFlag.toUpperCase()}`;
|
const flag = `TEST_${envFlag.toUpperCase()}`;
|
||||||
const enabled = !/^(|0|false)$/iu.test(process.env[flag] ?? '');
|
const enabled = !/^(|0|false)$/iu.test(process.env[flag] ?? '');
|
||||||
// eslint-disable-next-line jest/valid-describe-callback, jest/valid-title, jest/no-disabled-tests
|
return enabled ? describe : describe.skip;
|
||||||
return enabled ? describe(name, fn) : describe.skip(name, fn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user