Skip to content

Commit e280c7f

Browse files
committed
Implement cron job scheduling for lifecycle triggers and enhance job execution logic
1 parent 263aee2 commit e280c7f

File tree

1 file changed

+114
-1
lines changed

1 file changed

+114
-1
lines changed

src/management/lifecycle/lifecycle.service.ts

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,25 @@ import { ConfigObjectIdentitiesDTO, ConfigObjectSchemaDTO } from './_dto/config.
1313
import { validateOrReject, ValidationError } from 'class-validator';
1414
import { omit } from 'radash';
1515
import { IdentitiesCrudService } from '../identities/identities-crud.service';
16+
import { SchedulerRegistry } from '@nestjs/schedule';
17+
import { CronJob } from 'cron';
1618

1719
interface LifecycleSource {
1820
[source: string]: Partial<ConfigObjectIdentitiesDTO>[];
1921
}
2022

23+
type ConfigObjectIdentitiesDTOWithSource = Omit<ConfigObjectIdentitiesDTO, 'sources'> & {
24+
source: string;
25+
};
26+
2127
@Injectable()
2228
export class LifecycleService extends AbstractServiceSchema implements OnApplicationBootstrap, OnModuleInit {
2329
protected lifecycleSources: LifecycleSource = {};
2430

2531
public constructor(
2632
@InjectModel(Lifecycle.name) protected _model: Model<Lifecycle>,
2733
protected readonly identitiesService: IdentitiesCrudService,
34+
private schedulerRegistry: SchedulerRegistry,
2835
) {
2936
super();
3037
}
@@ -95,14 +102,120 @@ export class LifecycleService extends AbstractServiceSchema implements OnApplica
95102
if (!this.lifecycleSources[source]) {
96103
this.lifecycleSources[source] = [];
97104
}
98-
this.lifecycleSources[source].push(omit(idRule, ['sources'])); // Exclude sources from the stored rules
105+
106+
const rule = omit(idRule, ['sources']);
107+
if (rule.trigger) {
108+
this.logger.log(`Trigger found for source <${source}>: ${-rule.trigger}, installing cron job !`);
109+
110+
if (this.schedulerRegistry.doesExist('cron', `lifecycle-trigger-${source}`)) {
111+
this.logger.warn(`Cron job for source <${source}> already exists, skipping creation.`);
112+
continue;
113+
}
114+
115+
const cronExpression = this.convertSecondsToCron(-rule.trigger);
116+
this.logger.debug(`Creating cron job with pattern: ${cronExpression}`);
117+
118+
const job = new CronJob(cronExpression, this.runJob.bind(this, {
119+
source, // Pass the source to the job for context
120+
...rule,
121+
}));
122+
123+
this.schedulerRegistry.addCronJob(`lifecycle-trigger-${source}`, job);
124+
job.start();
125+
}
126+
127+
this.lifecycleSources[source].push(rule);
99128
}
100129
}
101130
}
102131

103132
this.logger.log('LifecycleService bootstraped');
104133
}
105134

135+
protected async runJob(rule: ConfigObjectIdentitiesDTOWithSource): Promise<void> {
136+
this.logger.debug(`Running LifecycleService job: <${JSON.stringify(rule)}>`);
137+
138+
try {
139+
const identities = await this.identitiesService.model.find({
140+
...rule.rules,
141+
lifecycle: rule.source,
142+
ignoreLifecycle: { $ne: true },
143+
});
144+
145+
this.logger.log(`Found ${identities.length} identities to process for trigger in source <${rule.source}>`);
146+
147+
for (const identity of identities) {
148+
const updated = await this.identitiesService.model.findOneAndUpdate(
149+
{ _id: identity._id },
150+
{ $set: { lifecycle: rule.target } },
151+
{ new: true }
152+
);
153+
154+
if (updated) {
155+
await this.create({
156+
refId: identity._id,
157+
lifecycle: rule.target,
158+
date: new Date(),
159+
});
160+
161+
this.logger.log(`Identity <${identity._id}> updated to lifecycle <${rule.target}> by trigger from source <${rule.source}>`);
162+
}
163+
}
164+
} catch (error) {
165+
this.logger.error(`Error in lifecycle trigger job for source <${rule.source}>:`, error.message, error.stack);
166+
}
167+
}
168+
169+
/**
170+
* Convert seconds to a proper cron expression
171+
* This method converts a duration in seconds to the most appropriate cron expression.
172+
* It optimizes for readability and performance by using the largest possible time unit.
173+
*
174+
* @param seconds - The number of seconds for the interval
175+
* @returns A cron expression string in the format "second minute hour day month dayOfWeek"
176+
*/
177+
private convertSecondsToCron(seconds: number): string {
178+
// Ensure we have a valid positive number
179+
const intervalSeconds = Math.max(1, Math.floor(seconds));
180+
181+
// If it's less than 60 seconds, use seconds
182+
if (intervalSeconds < 60) {
183+
return `*/${intervalSeconds} * * * * *`;
184+
}
185+
186+
// If it's exactly divisible by 60 and less than 3600, use minutes
187+
const minutes = intervalSeconds / 60;
188+
if (intervalSeconds % 60 === 0 && minutes < 60) {
189+
return `0 */${Math.floor(minutes)} * * * *`;
190+
}
191+
192+
// If it's exactly divisible by 3600 and less than 86400, use hours
193+
const hours = intervalSeconds / 3600;
194+
if (intervalSeconds % 3600 === 0 && hours < 24) {
195+
return `0 0 */${Math.floor(hours)} * * *`;
196+
}
197+
198+
// If it's exactly divisible by 86400, use days
199+
const days = intervalSeconds / 86400;
200+
if (intervalSeconds % 86400 === 0 && days <= 30) {
201+
return `0 0 0 */${Math.floor(days)} * *`;
202+
}
203+
204+
// For very large intervals or non-standard intervals, fall back to the most appropriate unit
205+
if (intervalSeconds >= 3600) {
206+
// Use hours for intervals >= 1 hour
207+
const hourInterval = Math.max(1, Math.floor(intervalSeconds / 3600));
208+
return `0 0 */${hourInterval} * * *`;
209+
} else if (intervalSeconds >= 60) {
210+
// Use minutes for intervals >= 1 minute
211+
const minuteInterval = Math.max(1, Math.floor(intervalSeconds / 60));
212+
return `0 */${minuteInterval} * * * *`;
213+
} else {
214+
// Fall back to seconds
215+
return `*/${intervalSeconds} * * * * *`;
216+
}
217+
}
218+
106219
/**
107220
* Load lifecycle rules from configuration files
108221
*

0 commit comments

Comments
 (0)