-
Notifications
You must be signed in to change notification settings - Fork 43
Open
Labels
documentationImprovements or additions to documentationImprovements or additions to documentationsolvedThe question was answered or the problem was solvedThe question was answered or the problem was solved
Description
After upgrading nestjs-cls
to version 6.0.1, I started encountering test failures in parts of my codebase that depend heavily on the @UseCls()
decorator—especially in places where execution occurs outside the typical request lifecycle, like background jobs.
This happens because there’s no HTTP request triggering the CLS context—something @UseCls()
expects by default. Even replacing the decorator with an explicit this.cls.run(...)
block didn’t solve the problem.
Test code
import { createMock } from '@golevelup/ts-jest';
import { BaseJobRunner } from './base-job-runner.service';
import { JobMetrics } from '@shared/apm/metrics';
import { CustomClsModule } from '@cls';
import {
ClsPluginTransactional,
NoOpTransactionalAdapter,
} from '@nestjs-cls/transactional';
import { Test } from '@nestjs/testing';
describe('BaseJobRunnerService', () => {
class FakeJobRunner extends BaseJobRunner {
async execute(fail: boolean) {
if (fail) throw new Error('test');
}
}
async function buildRunner() {
const app = await Test.createTestingModule({
imports: [
CustomClsModule.register({
clsTransaction: new ClsPluginTransactional({
adapter: new NoOpTransactionalAdapter({
tx: createMock(),
}),
}),
}),
],
providers: [FakeJobRunner],
}).compile();
return app.get(FakeJobRunner);
}
it('should report as nok if the job fails', async () => {
await buildRunner().then((jobRunner) => jobRunner.run(true));
// assert
});
});
CustomClsModule
export class CustomClsModule {
static register(
options: { clsTransaction?: ClsPluginTransactional } = {},
): Nest.DynamicModule {
const clsTransaction =
typeof options.clsTransaction === 'undefined'
? new ClsPluginTransactional({
imports: [TypeOrmModule],
adapter: new TransactionalAdapterTypeOrm({
dataSourceToken: getDataSourceToken(),
}),
enableTransactionProxy: true,
})
: options.clsTransaction;
return {
module: CustomClsModule,
global: true,
imports: [
ClsModule.forRoot({
global: true,
middleware: {
mount: true,
setup: (cls: CustomClsService, request: Request) => {
const storeId = Number(request.get('x-store-id'));
if (!isNaN(storeId)) cls.set('storeId', storeId);
cls.set('reqId', randomUUID());
},
},
plugins: [clsTransaction],
}),
],
providers: [
{
provide: CustomClsService,
useExisting: ClsService,
},
],
exports: [CustomClsService, ClsLogger],
};
}
}
BaseJobRunner class
export abstract class BaseJobRunner<T extends any[] = any[]> {
protected readonly logger: Logger;
constructor(
private readonly name: string,
protected readonly cls: CustomClsService,
) {
this.logger = new Logger(name);
}
@UseCls()
public async run(...args: T): Promise<void> {
await this.cls.run(async () => {
const startDate = new Date();
const start = startDate.getTime();
let result: 'ok' | 'nok' = 'ok';
this.logger.log(
new Log({
message: `Starting job ${this.name}`,
data: { startDate },
}),
);
try {
await this.execute(...args);
} catch (error: any) {
result = 'nok';
this.logger.error(
new ErrorLog({ error, message: `error executing job ${this.name}` }),
);
} finally {
const endDate = new Date();
const elapsed = endDate.getTime() - start;
// report result to metrics
this.logger.log(
new Log({
message: `Finishing job ${this.name}`,
data: { startDate, endDate },
}),
);
}
});
}
protected abstract execute(...args: T): Promise<void>;
}
Error trace
● BaseJobRunnerService › should report as ok if the job succeeds
ClsPluginsHooksHost not initialized
29 | it('should report as ok if the job succeeds', async () => {
30 | const jobRunner = new FakeJobRunner('test', createMock<ClsService>());
> 31 | await jobRunner.run(false);
| ^
32 | ...
33 |
34 |
at Function.getInstance (../node_modules/nestjs-cls/dist/src/lib/plugin/cls-plugin-hooks-host.js:27:19)
at Object.apply (../node_modules/nestjs-cls/dist/src/lib/cls-initializers/use-cls.decorator.js:20:81)
at Object.<anonymous> (shared/cron-job/base-job-runner.service.spec.ts:31:21)
Metadata
Metadata
Assignees
Labels
documentationImprovements or additions to documentationImprovements or additions to documentationsolvedThe question was answered or the problem was solvedThe question was answered or the problem was solved