Skip to content

Commit

Permalink
feat(nestjs-tools-lock): add non blocking method to wait for complete…
Browse files Browse the repository at this point in the history
… initialization
  • Loading branch information
getlarge committed Aug 7, 2024
1 parent 59e516d commit 8ad6142
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 27 deletions.
5 changes: 3 additions & 2 deletions packages/lock/src/lib/lock.module.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { DynamicModule, InjectionToken, Module, ModuleMetadata, Provider, Type } from '@nestjs/common';

import { LOCK_SERVICE_OPTIONS } from './constants';
import { LockService, LockServiceOptions } from './lock.service';
import { LockService } from './lock.service';
import { LockServiceOptions } from './types';

export type Injection = InjectionToken[];
export interface LockModuleModuleAsyncOptions extends Pick<ModuleMetadata, 'imports' | 'providers'> {
name?: string;
useClass?: Type;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
useFactory?: (...args: any[]) => LockServiceOptions;
useFactory?: (...args: any[]) => LockServiceOptions | Promise<LockServiceOptions>;
inject?: Injection;
}

Expand Down
71 changes: 46 additions & 25 deletions packages/lock/src/lib/lock.service.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
import { Inject, Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import IORedis, { Redis, RedisOptions } from 'ioredis';
import Redlock, { ExecutionResult, Lock, RedlockAbortSignal, ResourceLockedError, Settings } from 'redlock';
import IORedis, { Redis } from 'ioredis';
import Redlock, { ExecutionResult, Lock, RedlockAbortSignal, ResourceLockedError } from 'redlock';

import { LOCK_SERVICE_OPTIONS } from './constants';

export type LockResponse = {
unlock: () => Promise<void>;
lockId: string;
};

export type LockOptions = Partial<Settings>;

export interface LockServiceOptions {
redis: RedisOptions;
lock?: LockOptions;
}
import { LockOptions, LockServiceOptions } from './types';

@Injectable()
export class LockService implements OnModuleInit, OnModuleDestroy {
Expand All @@ -28,14 +17,52 @@ export class LockService implements OnModuleInit, OnModuleDestroy {

constructor(@Inject(LOCK_SERVICE_OPTIONS) readonly options: LockServiceOptions) {}

onModuleInit(): void {
async onModuleInit(): Promise<void> {
this.createConnection();
await this.waitUntilInitialized();
}

async onModuleDestroy(): Promise<void> {
await this.close();
}

private async waitUntilInitialized(timeout = 5000): Promise<void> {
return new Promise((resolve, reject) => {
const controller = new AbortController();
const signal = controller.signal;

const interval = setInterval(() => {
if (this.isInitialized) {
controller.abort();
resolve();
}
}, 150);

const timer = setTimeout(() => {
if (!this.isInitialized) {
controller.abort();
reject(new Error('Initialization timed out'));
}
}, timeout);

signal.addEventListener('abort', () => {
clearInterval(interval);
clearTimeout(timer);
});
});
}

get isInitialized(): boolean {
const validRedisStatus = ['connected', 'ready'];
return !!this.redis && validRedisStatus.includes(this.redis.status) && !!this.redlock;
}

checkInitialization(): void {
if (!this.isInitialized) {
throw new Error('Redis was not yet initialized');
}
}

errorHandler(error: unknown) {
// Ignore cases where a resource is explicitly marked as locked on a client.
if (error instanceof ResourceLockedError) {
Expand All @@ -51,31 +78,25 @@ export class LockService implements OnModuleInit, OnModuleDestroy {
// this.redlock.on('error', this.errorHandler);
}

isInitialized(): void {
if (!this.redis || !this.redlock) {
throw new Error('Redis was not yet initialized');
}
}

optimistic<T = unknown>(key: string, ttl: number, cb: (signal: RedlockAbortSignal) => Promise<T>): Promise<T> {
this.isInitialized();
this.checkInitialization();
return this.redlock.using<T>([key], ttl, cb);
}

lock(key: string, ttl: number): Promise<Lock> {
this.isInitialized();
this.checkInitialization();
return this.redlock.acquire([key], ttl);
}

async get(key: string, lockId: string): Promise<Lock | null> {
this.isInitialized();
this.checkInitialization();
const value = await this.redis.get(key);
// provide fake data for attempts and expiration as this lock would be used for release only
return value !== lockId ? null : new Lock(this.redlock, [key], lockId, [], 100);
}

async unlock(key: string, lockId: string): Promise<ExecutionResult> {
this.isInitialized();
this.checkInitialization();
const lock = await this.get(key, lockId);
if (!lock) {
throw new Error(`Lock ${key} - ${lockId} not found.`);
Expand Down
15 changes: 15 additions & 0 deletions packages/lock/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { RedisOptions } from 'ioredis';
import type { Settings } from 'redlock';

export type LockResponse = {
unlock: () => Promise<void>;
lockId: string;
};

export type LockOptions = Partial<Settings>;

export interface LockServiceOptions {
redis: RedisOptions;
lock?: LockOptions;
// waitUntilInitializedTimeout?: number;
}

0 comments on commit 8ad6142

Please sign in to comment.