@nest-omni/core 2.0.1-10 → 2.0.1-12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/setup/bootstrap.setup.js +34 -83
- package/setup/index.d.ts +5 -0
- package/setup/index.js +5 -0
- package/setup/mode.setup.d.ts +12 -0
- package/setup/mode.setup.js +60 -0
- package/setup/redis.lock.decorator.d.ts +5 -0
- package/setup/redis.lock.decorator.js +56 -0
- package/setup/redis.lock.service.d.ts +30 -0
- package/setup/redis.lock.service.js +185 -0
- package/setup/schedule.decorator.d.ts +21 -0
- package/setup/schedule.decorator.js +127 -0
- package/setup/worker.decorator.d.ts +14 -0
- package/setup/worker.decorator.js +130 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
package/setup/bootstrap.setup.js
CHANGED
|
@@ -16,6 +16,7 @@ const dotenv = require("dotenv");
|
|
|
16
16
|
const fs_1 = require("fs");
|
|
17
17
|
const path_1 = require("path");
|
|
18
18
|
const process = require("process");
|
|
19
|
+
const mode_setup_1 = require("./mode.setup");
|
|
19
20
|
function findValidRootPath() {
|
|
20
21
|
const getAppRootPath = () => {
|
|
21
22
|
if (require.main && require.main.filename) {
|
|
@@ -70,8 +71,9 @@ Sentry.init({
|
|
|
70
71
|
release: process.env.API_VERSION || '',
|
|
71
72
|
environment: process.env.NODE_ENV || 'unkown',
|
|
72
73
|
debug: process.env.NODE_ENV === 'dev',
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
sampleRate: 1.0,
|
|
75
|
+
tracesSampleRate: 0.01,
|
|
76
|
+
profileSessionSampleRate: 0.01,
|
|
75
77
|
profileLifecycle: 'trace',
|
|
76
78
|
sendDefaultPii: true,
|
|
77
79
|
integrations: [profiling_node_1.nodeProfilingIntegration],
|
|
@@ -116,79 +118,21 @@ crud_1.CrudConfigService.load({
|
|
|
116
118
|
},
|
|
117
119
|
},
|
|
118
120
|
});
|
|
119
|
-
const setupProcessHandlers = (app) => {
|
|
120
|
-
const logger = app.get(nestjs_pino_1.Logger);
|
|
121
|
-
process.on('uncaughtException', (error) => {
|
|
122
|
-
logger.fatal({ error, stack: error.stack, pid: process.pid }, 'Uncaught Exception');
|
|
123
|
-
process.exit(1);
|
|
124
|
-
});
|
|
125
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
126
|
-
logger.error({
|
|
127
|
-
reason: {
|
|
128
|
-
message: reason.message,
|
|
129
|
-
stack: reason.stack,
|
|
130
|
-
name: reason.name,
|
|
131
|
-
},
|
|
132
|
-
promise,
|
|
133
|
-
pid: process.pid,
|
|
134
|
-
}, 'Unhandled Rejection');
|
|
135
|
-
});
|
|
136
|
-
process.on('exit', (code) => {
|
|
137
|
-
logger.warn(`Process exiting with code ${code}`, {
|
|
138
|
-
uptime: process.uptime(),
|
|
139
|
-
memoryUsage: process.memoryUsage(),
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
};
|
|
143
|
-
const setupGracefulShutdown = (app) => {
|
|
144
|
-
const logger = app.get(nestjs_pino_1.Logger);
|
|
145
|
-
const shutdown = (signal) => __awaiter(void 0, void 0, void 0, function* () {
|
|
146
|
-
var _a;
|
|
147
|
-
try {
|
|
148
|
-
logger.warn(`Received ${signal}, starting graceful shutdown...`, {
|
|
149
|
-
uptime: process.uptime(),
|
|
150
|
-
connections: (_a = app.getHttpServer()) === null || _a === void 0 ? void 0 : _a.address(),
|
|
151
|
-
});
|
|
152
|
-
yield Promise.race([
|
|
153
|
-
app.close(),
|
|
154
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Shutdown timeout exceeded')), 15000)),
|
|
155
|
-
]);
|
|
156
|
-
logger.log('Application successfully closed', {
|
|
157
|
-
resourcesReleased: true,
|
|
158
|
-
pid: process.pid,
|
|
159
|
-
});
|
|
160
|
-
process.exit(0);
|
|
161
|
-
}
|
|
162
|
-
catch (err) {
|
|
163
|
-
logger.error('Graceful shutdown failed', {
|
|
164
|
-
error: {
|
|
165
|
-
message: err.message,
|
|
166
|
-
stack: err.stack,
|
|
167
|
-
name: err.name,
|
|
168
|
-
},
|
|
169
|
-
critical: true,
|
|
170
|
-
pid: process.pid,
|
|
171
|
-
});
|
|
172
|
-
process.exit(1);
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
process.on('SIGQUIT', () => shutdown('SIGQUIT'));
|
|
176
|
-
process.on('SIGHUP', () => shutdown('SIGHUP'));
|
|
177
|
-
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
178
|
-
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
179
|
-
};
|
|
180
121
|
function bootstrapSetup(AppModule, SetupSwagger) {
|
|
181
122
|
return __awaiter(this, void 0, void 0, function* () {
|
|
182
123
|
(0, typeorm_transactional_1.initializeTransactionalContext)();
|
|
124
|
+
const shouldStartHttp = (0, mode_setup_1.shouldStartHttpServer)();
|
|
183
125
|
const app = yield core_1.NestFactory.create(AppModule, {
|
|
184
126
|
bufferLogs: true,
|
|
127
|
+
abortOnError: false,
|
|
128
|
+
autoFlushLogs: true,
|
|
185
129
|
});
|
|
186
130
|
(0, class_validator_1.useContainer)(app.select(AppModule), { fallbackOnErrors: true });
|
|
187
131
|
const configService = app.select(__1.ServiceRegistryModule).get(__1.ApiConfigService);
|
|
188
132
|
const logger = app.get(nestjs_pino_1.Logger);
|
|
189
|
-
|
|
133
|
+
logger.log(`Application Mode: ${(0, mode_setup_1.getModeDescription)()}`);
|
|
134
|
+
logger.log(`Environment: ${process.env.NODE_ENV || 'unknown'}`);
|
|
190
135
|
app.enableShutdownHooks();
|
|
191
|
-
setupGracefulShutdown(app);
|
|
192
136
|
app.enableVersioning();
|
|
193
137
|
app.enable('trust proxy');
|
|
194
138
|
app.use(bodyParse.json({ limit: '50mb' }), new nestjs_cls_1.ClsMiddleware({}).use, (0, __1.RequestIdMiddleware)(), (0, __1.PowerByMiddleware)(), (0, __1.OmniAuthMiddleware)(), compression());
|
|
@@ -202,26 +146,33 @@ function bootstrapSetup(AppModule, SetupSwagger) {
|
|
|
202
146
|
stopAtFirstError: true,
|
|
203
147
|
validationError: { target: false, value: false },
|
|
204
148
|
}));
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
149
|
+
if (shouldStartHttp) {
|
|
150
|
+
if (configService.documentationEnabled && SetupSwagger) {
|
|
151
|
+
SetupSwagger(app, configService.documentationPath);
|
|
152
|
+
logger.log(`Swagger docs available at ${configService.documentationPath}`);
|
|
153
|
+
}
|
|
154
|
+
if (configService.viewsEnabled) {
|
|
155
|
+
app.setBaseViewsDir((0, path_1.join)(__1.ApiConfigService.rootPath, 'views'));
|
|
156
|
+
app.setViewEngine('ejs');
|
|
157
|
+
logger.log('View engine initialized');
|
|
158
|
+
}
|
|
159
|
+
if (configService.sessionEnabled) {
|
|
160
|
+
app.use(session(configService.sessionConfig));
|
|
161
|
+
logger.log('Session middleware enabled');
|
|
162
|
+
}
|
|
163
|
+
if (configService.corsEnabled) {
|
|
164
|
+
app.enableCors(configService.corsConfig);
|
|
165
|
+
logger.log('CORS configuration applied');
|
|
166
|
+
}
|
|
167
|
+
const port = configService.appConfig.port;
|
|
168
|
+
yield app.listen(port);
|
|
169
|
+
logger.log(`HTTP Server running on ${yield app.getUrl()}`);
|
|
217
170
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
logger.log('
|
|
171
|
+
else {
|
|
172
|
+
logger.log('Running in Worker-only mode - HTTP server not started');
|
|
173
|
+
logger.log('Application is ready to process background tasks');
|
|
174
|
+
yield app.init();
|
|
221
175
|
}
|
|
222
|
-
const port = configService.appConfig.port;
|
|
223
|
-
yield app.listen(port);
|
|
224
|
-
logger.log(`Server running on ${yield app.getUrl()}`);
|
|
225
176
|
return app;
|
|
226
177
|
});
|
|
227
178
|
}
|
package/setup/index.d.ts
CHANGED
package/setup/index.js
CHANGED
|
@@ -15,3 +15,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./bootstrap.setup"), exports);
|
|
18
|
+
__exportStar(require("./mode.setup"), exports);
|
|
19
|
+
__exportStar(require("./worker.decorator"), exports);
|
|
20
|
+
__exportStar(require("./schedule.decorator"), exports);
|
|
21
|
+
__exportStar(require("./redis.lock.service"), exports);
|
|
22
|
+
__exportStar(require("./redis.lock.decorator"), exports);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare enum ApplicationMode {
|
|
2
|
+
HTTP = "http",
|
|
3
|
+
WORKER = "worker",
|
|
4
|
+
HYBRID = "hybrid"
|
|
5
|
+
}
|
|
6
|
+
export declare function getApplicationMode(): ApplicationMode;
|
|
7
|
+
export declare function shouldProcessQueues(): boolean;
|
|
8
|
+
export declare function shouldStartHttpServer(): boolean;
|
|
9
|
+
export declare function isHttpMode(): boolean;
|
|
10
|
+
export declare function isWorkerMode(): boolean;
|
|
11
|
+
export declare function isHybridMode(): boolean;
|
|
12
|
+
export declare function getModeDescription(): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApplicationMode = void 0;
|
|
4
|
+
exports.getApplicationMode = getApplicationMode;
|
|
5
|
+
exports.shouldProcessQueues = shouldProcessQueues;
|
|
6
|
+
exports.shouldStartHttpServer = shouldStartHttpServer;
|
|
7
|
+
exports.isHttpMode = isHttpMode;
|
|
8
|
+
exports.isWorkerMode = isWorkerMode;
|
|
9
|
+
exports.isHybridMode = isHybridMode;
|
|
10
|
+
exports.getModeDescription = getModeDescription;
|
|
11
|
+
var ApplicationMode;
|
|
12
|
+
(function (ApplicationMode) {
|
|
13
|
+
ApplicationMode["HTTP"] = "http";
|
|
14
|
+
ApplicationMode["WORKER"] = "worker";
|
|
15
|
+
ApplicationMode["HYBRID"] = "hybrid";
|
|
16
|
+
})(ApplicationMode || (exports.ApplicationMode = ApplicationMode = {}));
|
|
17
|
+
function getApplicationMode() {
|
|
18
|
+
const mode = (process.env.APP_MODE || process.env.MODE || 'hybrid')
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.trim();
|
|
21
|
+
switch (mode) {
|
|
22
|
+
case 'http':
|
|
23
|
+
return ApplicationMode.HTTP;
|
|
24
|
+
case 'worker':
|
|
25
|
+
return ApplicationMode.WORKER;
|
|
26
|
+
case 'hybrid':
|
|
27
|
+
default:
|
|
28
|
+
return ApplicationMode.HYBRID;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function shouldProcessQueues() {
|
|
32
|
+
const mode = getApplicationMode();
|
|
33
|
+
return mode === ApplicationMode.WORKER || mode === ApplicationMode.HYBRID;
|
|
34
|
+
}
|
|
35
|
+
function shouldStartHttpServer() {
|
|
36
|
+
const mode = getApplicationMode();
|
|
37
|
+
return mode === ApplicationMode.HTTP || mode === ApplicationMode.HYBRID;
|
|
38
|
+
}
|
|
39
|
+
function isHttpMode() {
|
|
40
|
+
return getApplicationMode() === ApplicationMode.HTTP;
|
|
41
|
+
}
|
|
42
|
+
function isWorkerMode() {
|
|
43
|
+
return getApplicationMode() === ApplicationMode.WORKER;
|
|
44
|
+
}
|
|
45
|
+
function isHybridMode() {
|
|
46
|
+
return getApplicationMode() === ApplicationMode.HYBRID;
|
|
47
|
+
}
|
|
48
|
+
function getModeDescription() {
|
|
49
|
+
const mode = getApplicationMode();
|
|
50
|
+
switch (mode) {
|
|
51
|
+
case ApplicationMode.HTTP:
|
|
52
|
+
return 'HTTP-only mode (no background workers)';
|
|
53
|
+
case ApplicationMode.WORKER:
|
|
54
|
+
return 'Worker-only mode (no HTTP server)';
|
|
55
|
+
case ApplicationMode.HYBRID:
|
|
56
|
+
return 'Hybrid mode (HTTP server + background workers)';
|
|
57
|
+
default:
|
|
58
|
+
return 'Unknown mode';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LockOptions } from './redis.lock.service';
|
|
2
|
+
export declare function UseRedisLock(lockKey: string, options?: LockOptions & {
|
|
3
|
+
skipReturnValue?: any;
|
|
4
|
+
}): MethodDecorator;
|
|
5
|
+
export declare function UseRedisLockOrSkip(lockKey: string, ttl?: number): MethodDecorator;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.UseRedisLock = UseRedisLock;
|
|
13
|
+
exports.UseRedisLockOrSkip = UseRedisLockOrSkip;
|
|
14
|
+
const common_1 = require("@nestjs/common");
|
|
15
|
+
function UseRedisLock(lockKey, options = {}) {
|
|
16
|
+
return function (target, propertyKey, descriptor) {
|
|
17
|
+
const originalMethod = descriptor.value;
|
|
18
|
+
const logger = new common_1.Logger(`RedisLock:${String(propertyKey)}`);
|
|
19
|
+
descriptor.value = function (...args) {
|
|
20
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
21
|
+
var _a;
|
|
22
|
+
const lockService = this.lockService;
|
|
23
|
+
if (!lockService) {
|
|
24
|
+
logger.error(`RedisLockService not found on ${target.constructor.name}. ` +
|
|
25
|
+
`Please inject RedisLockService as 'lockService' in the constructor.`);
|
|
26
|
+
throw new Error(`RedisLockService not found. Please inject it as 'lockService'.`);
|
|
27
|
+
}
|
|
28
|
+
const skipReturnValue = (_a = options.skipReturnValue) !== null && _a !== void 0 ? _a : null;
|
|
29
|
+
const lockResult = yield lockService.acquireLock(lockKey, options);
|
|
30
|
+
if (!lockResult.acquired) {
|
|
31
|
+
logger.log(`Lock '${lockKey}' is already held, skipping execution of ${String(propertyKey)}`);
|
|
32
|
+
return skipReturnValue;
|
|
33
|
+
}
|
|
34
|
+
logger.debug(`Lock '${lockKey}' acquired, executing ${String(propertyKey)}`);
|
|
35
|
+
try {
|
|
36
|
+
return yield originalMethod.apply(this, args);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
logger.error(`Error executing ${String(propertyKey)} with lock '${lockKey}':`, error);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
yield lockService.releaseLock(lockKey, lockResult.lockValue, options.keyPrefix);
|
|
44
|
+
logger.debug(`Lock '${lockKey}' released`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
return descriptor;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function UseRedisLockOrSkip(lockKey, ttl = 300000) {
|
|
52
|
+
return UseRedisLock(lockKey, {
|
|
53
|
+
ttl,
|
|
54
|
+
skipReturnValue: false,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { RedisService } from '@liaoliaots/nestjs-redis';
|
|
3
|
+
export interface LockOptions {
|
|
4
|
+
ttl?: number;
|
|
5
|
+
retryCount?: number;
|
|
6
|
+
retryDelay?: number;
|
|
7
|
+
keyPrefix?: string;
|
|
8
|
+
throwOnFailure?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface LockResult {
|
|
11
|
+
acquired: boolean;
|
|
12
|
+
lockValue?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class RedisLockService implements OnModuleInit {
|
|
16
|
+
private readonly redisService;
|
|
17
|
+
private readonly logger;
|
|
18
|
+
private redis;
|
|
19
|
+
private readonly defaultOptions;
|
|
20
|
+
constructor(redisService: RedisService);
|
|
21
|
+
onModuleInit(): void;
|
|
22
|
+
private generateLockValue;
|
|
23
|
+
private buildLockKey;
|
|
24
|
+
private sleep;
|
|
25
|
+
acquireLock(key: string, options?: LockOptions): Promise<LockResult>;
|
|
26
|
+
releaseLock(key: string, lockValue: string, keyPrefix?: string): Promise<boolean>;
|
|
27
|
+
extendLock(key: string, lockValue: string, ttl: number, keyPrefix?: string): Promise<boolean>;
|
|
28
|
+
isLocked(key: string, keyPrefix?: string): Promise<boolean>;
|
|
29
|
+
withLock<T>(key: string, fn: () => Promise<T>, options?: LockOptions): Promise<T | null>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
12
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
13
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
14
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
15
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
16
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
17
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
var RedisLockService_1;
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.RedisLockService = void 0;
|
|
23
|
+
const common_1 = require("@nestjs/common");
|
|
24
|
+
const nestjs_redis_1 = require("@liaoliaots/nestjs-redis");
|
|
25
|
+
let RedisLockService = RedisLockService_1 = class RedisLockService {
|
|
26
|
+
constructor(redisService) {
|
|
27
|
+
this.redisService = redisService;
|
|
28
|
+
this.logger = new common_1.Logger(RedisLockService_1.name);
|
|
29
|
+
this.defaultOptions = {
|
|
30
|
+
ttl: 300000,
|
|
31
|
+
retryCount: 0,
|
|
32
|
+
retryDelay: 100,
|
|
33
|
+
keyPrefix: 'lock',
|
|
34
|
+
throwOnFailure: false,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
onModuleInit() {
|
|
38
|
+
try {
|
|
39
|
+
this.redis = this.redisService.getOrThrow();
|
|
40
|
+
this.logger.log('RedisLockService initialized successfully');
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
this.logger.error('Failed to initialize RedisLockService', error);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
generateLockValue() {
|
|
48
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
49
|
+
}
|
|
50
|
+
buildLockKey(key, prefix = 'lock') {
|
|
51
|
+
return `${prefix}:${key}`;
|
|
52
|
+
}
|
|
53
|
+
sleep(ms) {
|
|
54
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
55
|
+
}
|
|
56
|
+
acquireLock(key_1) {
|
|
57
|
+
return __awaiter(this, arguments, void 0, function* (key, options = {}) {
|
|
58
|
+
const opts = Object.assign(Object.assign({}, this.defaultOptions), options);
|
|
59
|
+
const lockKey = this.buildLockKey(key, opts.keyPrefix);
|
|
60
|
+
const lockValue = this.generateLockValue();
|
|
61
|
+
let attempts = 0;
|
|
62
|
+
const maxAttempts = opts.retryCount + 1;
|
|
63
|
+
while (attempts < maxAttempts) {
|
|
64
|
+
try {
|
|
65
|
+
const result = yield this.redis.set(lockKey, lockValue, 'PX', opts.ttl, 'NX');
|
|
66
|
+
if (result === 'OK') {
|
|
67
|
+
this.logger.debug(`Lock acquired: ${lockKey} (value: ${lockValue}, ttl: ${opts.ttl}ms)`);
|
|
68
|
+
return { acquired: true, lockValue };
|
|
69
|
+
}
|
|
70
|
+
attempts++;
|
|
71
|
+
if (attempts < maxAttempts) {
|
|
72
|
+
this.logger.debug(`Lock acquisition failed for ${lockKey}, retrying... (${attempts}/${opts.retryCount})`);
|
|
73
|
+
yield this.sleep(opts.retryDelay);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const errorMessage = `Error acquiring lock for ${lockKey}: ${error.message}`;
|
|
78
|
+
this.logger.error(errorMessage, error.stack);
|
|
79
|
+
if (opts.throwOnFailure) {
|
|
80
|
+
throw new Error(errorMessage);
|
|
81
|
+
}
|
|
82
|
+
return { acquired: false, error: errorMessage };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const failureMessage = `Failed to acquire lock for ${lockKey} after ${attempts} attempts`;
|
|
86
|
+
this.logger.warn(failureMessage);
|
|
87
|
+
if (opts.throwOnFailure) {
|
|
88
|
+
throw new Error(failureMessage);
|
|
89
|
+
}
|
|
90
|
+
return { acquired: false, error: failureMessage };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
releaseLock(key_1, lockValue_1) {
|
|
94
|
+
return __awaiter(this, arguments, void 0, function* (key, lockValue, keyPrefix = 'lock') {
|
|
95
|
+
const lockKey = this.buildLockKey(key, keyPrefix);
|
|
96
|
+
try {
|
|
97
|
+
const script = `
|
|
98
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
99
|
+
return redis.call("del", KEYS[1])
|
|
100
|
+
else
|
|
101
|
+
return 0
|
|
102
|
+
end
|
|
103
|
+
`;
|
|
104
|
+
const result = yield this.redis.eval(script, 1, lockKey, lockValue);
|
|
105
|
+
if (result === 1) {
|
|
106
|
+
this.logger.debug(`Lock released: ${lockKey}`);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
this.logger.warn(`Lock release failed: ${lockKey} (lock value mismatch or already expired)`);
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
this.logger.error(`Error releasing lock for ${lockKey}: ${error.message}`, error.stack);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
extendLock(key_1, lockValue_1, ttl_1) {
|
|
121
|
+
return __awaiter(this, arguments, void 0, function* (key, lockValue, ttl, keyPrefix = 'lock') {
|
|
122
|
+
const lockKey = this.buildLockKey(key, keyPrefix);
|
|
123
|
+
try {
|
|
124
|
+
const script = `
|
|
125
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
126
|
+
return redis.call("pexpire", KEYS[1], ARGV[2])
|
|
127
|
+
else
|
|
128
|
+
return 0
|
|
129
|
+
end
|
|
130
|
+
`;
|
|
131
|
+
const result = yield this.redis.eval(script, 1, lockKey, lockValue, ttl);
|
|
132
|
+
if (result === 1) {
|
|
133
|
+
this.logger.debug(`Lock extended: ${lockKey} (new ttl: ${ttl}ms)`);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
this.logger.warn(`Lock extension failed: ${lockKey} (lock value mismatch or doesn't exist)`);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
this.logger.error(`Error extending lock for ${lockKey}: ${error.message}`, error.stack);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
isLocked(key_1) {
|
|
148
|
+
return __awaiter(this, arguments, void 0, function* (key, keyPrefix = 'lock') {
|
|
149
|
+
const lockKey = this.buildLockKey(key, keyPrefix);
|
|
150
|
+
try {
|
|
151
|
+
const exists = yield this.redis.exists(lockKey);
|
|
152
|
+
return exists === 1;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
this.logger.error(`Error checking lock existence for ${lockKey}: ${error.message}`, error.stack);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
withLock(key_1, fn_1) {
|
|
161
|
+
return __awaiter(this, arguments, void 0, function* (key, fn, options = {}) {
|
|
162
|
+
const lockResult = yield this.acquireLock(key, options);
|
|
163
|
+
if (!lockResult.acquired) {
|
|
164
|
+
this.logger.warn(`Could not acquire lock for ${key}, skipping execution`);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const result = yield fn();
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
this.logger.error(`Error executing function with lock ${key}:`, error);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
yield this.releaseLock(key, lockResult.lockValue, options.keyPrefix);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
exports.RedisLockService = RedisLockService;
|
|
182
|
+
exports.RedisLockService = RedisLockService = RedisLockService_1 = __decorate([
|
|
183
|
+
(0, common_1.Injectable)(),
|
|
184
|
+
__metadata("design:paramtypes", [nestjs_redis_1.RedisService])
|
|
185
|
+
], RedisLockService);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CronOptions } from '@nestjs/schedule';
|
|
2
|
+
export declare function WorkerCron(cronTime: string | Date, options?: CronOptions): MethodDecorator;
|
|
3
|
+
export declare function WorkerInterval(timeout: number, options?: {
|
|
4
|
+
name?: string;
|
|
5
|
+
}): MethodDecorator;
|
|
6
|
+
export declare function WorkerTimeout(timeout: number, options?: {
|
|
7
|
+
name?: string;
|
|
8
|
+
}): MethodDecorator;
|
|
9
|
+
export declare function WorkerCronWithLock(cronTime: string | Date, lockKey: string, lockTtl?: number, cronOptions?: CronOptions): MethodDecorator;
|
|
10
|
+
export declare function WorkerIntervalWithLock(timeout: number, lockKey: string, lockTtl?: number, intervalOptions?: {
|
|
11
|
+
name?: string;
|
|
12
|
+
}): MethodDecorator;
|
|
13
|
+
export declare function WorkerTimeoutWithLock(timeout: number, lockKey: string, lockTtl?: number, timeoutOptions?: {
|
|
14
|
+
name?: string;
|
|
15
|
+
}): MethodDecorator;
|
|
16
|
+
export declare function WorkerCronAdvanced(cronTime: string | Date, lockKey: string, options?: {
|
|
17
|
+
lockTtl?: number;
|
|
18
|
+
cronOptions?: CronOptions;
|
|
19
|
+
logExecution?: boolean;
|
|
20
|
+
onError?: (error: Error) => void;
|
|
21
|
+
}): MethodDecorator;
|