Skip to content

Commit

Permalink
refactor: lock subsystem into typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
fredriklindberg committed Jan 27, 2024
1 parent b07cba0 commit 29fd9ab
Show file tree
Hide file tree
Showing 12 changed files with 241 additions and 165 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@types/node": "^20.5.0",
"@types/pg": "^8.10.9",
"@types/port-numbers": "^5.0.0",
"@types/redlock": "^4.0.7",
"@types/sinon": "^10.0.17",
"@types/ssh2": "^1.11.14",
"@types/sshpk": "^1.17.2",
Expand Down
95 changes: 0 additions & 95 deletions src/lock/index.js

This file was deleted.

103 changes: 103 additions & 0 deletions src/lock/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import assert from 'assert/strict';
import RedisLockProvider from './redis-lock-provider.js';
import MemoryLockProvider from './memory-lock-provider.js';
import { Logger } from '../logger.js';
import LockProvider, { ProviderLock } from './lock-provider.js';

class Lock {
private _lock: ProviderLock;
private resource: string;
private logger: any;

constructor(resource: string, lock: ProviderLock, logger: any) {
this._lock = lock;
this.resource = resource;
this.logger = logger;
}

public locked(): boolean {
return this._lock.active();
}

public async unlock(): Promise<boolean> {
return this._lock.unlock()
.catch((err) => {
this.logger.error({
message: `failed to unlock resource ${this.resource}: ${err.message}`,
operation: 'unlock',
});
return false;
})
.then(() => {
this.logger.isTraceEnabled() &&
this.logger.trace({
operation: 'unlock',
resource: this.resource,
});
return true;
});
}
}
export { Lock };

export type LockType = "redis" | "mem" | "none";
export type LockServiceOpts = {
callback: (err?: Error) => void;
redisUrl?: URL;
};

class LockService {
private logger: any;
private lockProvider!: LockProvider;

constructor(type: LockType, opts: LockServiceOpts) {
this.logger = Logger("lock-service");

switch (type) {
case 'redis':
this.lockProvider = new RedisLockProvider({
redisUrl: <URL>opts.redisUrl,
callback: (err) => {
typeof opts.callback === 'function' && process.nextTick(() => opts.callback(err));
}
});
break;
case 'none':
case 'mem':
this.lockProvider = new MemoryLockProvider();
typeof opts.callback === 'function' && process.nextTick(() => opts.callback());
break;
default:
assert.fail(`Unknown lock ${type}`);
}
}

async destroy(): Promise<void> {
await this.lockProvider.destroy();
}

async lock(resource: string): Promise<Lock | false> {
try {
const lock = await this.lockProvider.lock(`lock:${resource}`);
if (lock == null) {
throw new Error(`lock provider returned null lock`);
}

this.logger.isTraceEnabled() &&
this.logger.trace({
operation: 'lock',
resource,
result: lock != null,
});
return new Lock(resource, lock, this.logger);
} catch (e: any) {
this.logger.error({
message: `failed to obtain lock on ${resource}: ${e.message}`,
operation: 'lock',
});
return false;
}
}
}

export default LockService;
37 changes: 0 additions & 37 deletions src/lock/inmem-lock.js

This file was deleted.

10 changes: 10 additions & 0 deletions src/lock/lock-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

export interface ProviderLock {
active: () => boolean;
unlock: () => Promise<void>;
}

export default abstract class LockProvider {
public abstract lock(resource: string): Promise<ProviderLock | null>;
public abstract destroy(): Promise<void>;
}
43 changes: 43 additions & 0 deletions src/lock/memory-lock-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Mutex from "../utils/mutex.js";
import LockProvider, { ProviderLock } from "./lock-provider.js";

class MemoryLockProvider implements LockProvider {
private _locks: { [key: string]: Mutex };
private _abort: AbortController;

constructor() {
this._locks = {};
this._abort = new AbortController();
}

public async lock(resource: string): Promise<ProviderLock | null> {
this._locks[resource] ??= new Mutex();
const mutex = this._locks[resource];

try {
const locked = await mutex.acquire(this._abort.signal);
if (!locked) {
return null;
}
return {
active: () => { return true },
unlock: async () => {
mutex.release();
if (!mutex.locked()) {
delete this._locks[resource];
}
}
}
} catch (e: any) {
delete this._locks[resource];
return null
}
}

public async destroy(): Promise<void> {
this._abort.abort();
this._locks = {};
}
}

export default MemoryLockProvider;
Loading

0 comments on commit 29fd9ab

Please sign in to comment.