Skip to content

Commit e9f178d

Browse files
y4nderclaude
andcommitted
feat: add enable/disable toggle for Moodle sync cron job (#401) (#402)
Extend the sync schedule endpoint to support an `enabled` flag persisted via SystemConfig. The scheduler uses CronJob stop/start to fully disable the cron while keeping manual sync available. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c97c8db commit e9f178d

7 files changed

Lines changed: 98 additions & 11 deletions

File tree

src/modules/moodle/controllers/moodle-sync.controller.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,23 @@ export class MoodleSyncController {
218218
async UpdateSyncSchedule(
219219
@Body() dto: UpdateSyncScheduleDto,
220220
): Promise<SyncScheduleResponseDto> {
221-
await this.syncScheduler.updateSchedule(dto.intervalMinutes);
221+
if (dto.intervalMinutes === undefined && dto.enabled === undefined) {
222+
throw new HttpException(
223+
{
224+
error: 'At least one of intervalMinutes or enabled must be provided',
225+
},
226+
HttpStatus.BAD_REQUEST,
227+
);
228+
}
229+
230+
if (dto.intervalMinutes !== undefined) {
231+
await this.syncScheduler.updateSchedule(dto.intervalMinutes);
232+
}
233+
234+
if (dto.enabled !== undefined) {
235+
await this.syncScheduler.setEnabled(dto.enabled);
236+
}
237+
222238
return this.syncScheduler.getSchedule();
223239
}
224240
}
Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1-
import { ApiProperty } from '@nestjs/swagger';
2-
import { IsInt, Min } from 'class-validator';
1+
import { ApiPropertyOptional } from '@nestjs/swagger';
2+
import { IsBoolean, IsInt, IsOptional, Min } from 'class-validator';
33
import { MOODLE_SYNC_MIN_INTERVAL_MINUTES } from '../../schedulers/moodle-sync.constants';
44

55
export class UpdateSyncScheduleDto {
6-
@ApiProperty({
6+
@ApiPropertyOptional({
77
description: `Sync interval in minutes (minimum ${MOODLE_SYNC_MIN_INTERVAL_MINUTES})`,
88
example: 60,
99
minimum: MOODLE_SYNC_MIN_INTERVAL_MINUTES,
1010
})
11+
@IsOptional()
1112
@IsInt()
1213
@Min(MOODLE_SYNC_MIN_INTERVAL_MINUTES)
13-
intervalMinutes: number;
14+
intervalMinutes?: number;
15+
16+
@ApiPropertyOptional({
17+
description: 'Enable or disable the sync cron job',
18+
})
19+
@IsOptional()
20+
@IsBoolean()
21+
enabled?: boolean;
1422
}

src/modules/moodle/dto/responses/sync-schedule.response.dto.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ export class SyncScheduleResponseDto {
99

1010
@ApiPropertyOptional()
1111
nextExecution: string | null;
12+
13+
@ApiProperty({ description: 'Whether the sync cron job is enabled' })
14+
enabled: boolean;
1215
}

src/modules/moodle/schedulers/moodle-sync.constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ export const MOODLE_SYNC_JOB_NAME = 'moodle-sync-cron';
22

33
export const MOODLE_SYNC_CONFIG_KEY = 'MOODLE_SYNC_INTERVAL_MINUTES';
44

5+
export const MOODLE_SYNC_ENABLED_CONFIG_KEY = 'MOODLE_SYNC_ENABLED';
6+
57
export const MOODLE_SYNC_MIN_INTERVAL_MINUTES = 30;
68

79
export const MOODLE_SYNC_INTERVAL_DEFAULTS: Record<string, number> = {

src/modules/moodle/schedulers/moodle-sync.scheduler.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { env } from 'src/configurations/env';
1010
import {
1111
MOODLE_SYNC_JOB_NAME,
1212
MOODLE_SYNC_CONFIG_KEY,
13+
MOODLE_SYNC_ENABLED_CONFIG_KEY,
1314
MOODLE_SYNC_INTERVAL_DEFAULTS,
1415
MOODLE_SYNC_MIN_INTERVAL_MINUTES,
1516
minutesToCron,
@@ -20,6 +21,7 @@ export class MoodleSyncScheduler implements OnModuleInit {
2021
private readonly logger = new Logger(MoodleSyncScheduler.name);
2122
private currentIntervalMinutes: number;
2223
private currentCronExpression: string;
24+
private isEnabled: boolean;
2325

2426
constructor(
2527
@InjectQueue(QueueName.MOODLE_SYNC) private readonly syncQueue: Queue,
@@ -29,18 +31,20 @@ export class MoodleSyncScheduler implements OnModuleInit {
2931

3032
async onModuleInit() {
3133
const interval = await this.resolveInterval();
34+
const enabled = await this.resolveEnabled();
3235
this.currentIntervalMinutes = interval;
3336
this.currentCronExpression = minutesToCron(interval);
37+
this.isEnabled = enabled;
3438

3539
const job = CronJob.from({
3640
cronTime: this.currentCronExpression,
3741
onTick: () => this.handleScheduledSync(),
38-
start: true,
42+
start: enabled,
3943
});
4044

4145
this.schedulerRegistry.addCronJob(MOODLE_SYNC_JOB_NAME, job);
4246
this.logger.log(
43-
`Sync scheduler initialized: every ${interval}min (${this.currentCronExpression})`,
47+
`Sync scheduler initialized: every ${interval}min (${this.currentCronExpression}), enabled=${enabled}`,
4448
);
4549
}
4650

@@ -56,7 +60,7 @@ export class MoodleSyncScheduler implements OnModuleInit {
5660
const job = CronJob.from({
5761
cronTime: cronExpression,
5862
onTick: () => this.handleScheduledSync(),
59-
start: true,
63+
start: this.isEnabled,
6064
});
6165

6266
this.schedulerRegistry.addCronJob(MOODLE_SYNC_JOB_NAME, job);
@@ -89,17 +93,49 @@ export class MoodleSyncScheduler implements OnModuleInit {
8993
intervalMinutes: number;
9094
cronExpression: string;
9195
nextExecution: string | null;
96+
enabled: boolean;
9297
} {
9398
const job = this.schedulerRegistry.getCronJob(MOODLE_SYNC_JOB_NAME);
9499
const nextDate = job.nextDate();
95100

96101
return {
97102
intervalMinutes: this.currentIntervalMinutes,
98103
cronExpression: this.currentCronExpression,
99-
nextExecution: nextDate?.toISO() ?? null,
104+
nextExecution: this.isEnabled ? (nextDate?.toISO() ?? null) : null,
105+
enabled: this.isEnabled,
100106
};
101107
}
102108

109+
async setEnabled(enabled: boolean): Promise<void> {
110+
const job = this.schedulerRegistry.getCronJob(MOODLE_SYNC_JOB_NAME);
111+
112+
if (enabled && !this.isEnabled) {
113+
job.start();
114+
this.logger.log('Sync cron job enabled');
115+
} else if (!enabled && this.isEnabled) {
116+
await job.stop();
117+
this.logger.log('Sync cron job disabled');
118+
}
119+
120+
const fork = this.em.fork();
121+
const config = await fork.findOne(SystemConfig, {
122+
key: MOODLE_SYNC_ENABLED_CONFIG_KEY,
123+
});
124+
125+
if (config) {
126+
config.value = String(enabled);
127+
} else {
128+
const newConfig = new SystemConfig();
129+
newConfig.key = MOODLE_SYNC_ENABLED_CONFIG_KEY;
130+
newConfig.value = String(enabled);
131+
newConfig.description = 'Whether the Moodle sync cron job is enabled';
132+
fork.persist(newConfig);
133+
}
134+
await fork.flush();
135+
136+
this.isEnabled = enabled;
137+
}
138+
103139
private async handleScheduledSync() {
104140
try {
105141
await this.syncQueue.add(
@@ -164,4 +200,21 @@ export class MoodleSyncScheduler implements OnModuleInit {
164200
);
165201
return defaultInterval;
166202
}
203+
204+
private async resolveEnabled(): Promise<boolean> {
205+
try {
206+
const fork = this.em.fork();
207+
const config = await fork.findOne(SystemConfig, {
208+
key: MOODLE_SYNC_ENABLED_CONFIG_KEY,
209+
});
210+
if (config?.value) {
211+
return config.value === 'true';
212+
}
213+
} catch {
214+
this.logger.warn(
215+
'Could not read sync enabled state from database, defaulting to enabled',
216+
);
217+
}
218+
return true;
219+
}
167220
}

src/seeders/infrastructure/system-config.seeder.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export class SystemConfigSeeder extends Seeder {
2020
value: '60',
2121
description: 'Interval for Moodle synchronization in minutes.',
2222
},
23+
{
24+
key: 'MOODLE_SYNC_ENABLED',
25+
value: 'true',
26+
description: 'Whether the Moodle sync cron job is enabled.',
27+
},
2328
{
2429
key: 'SENTIMENT_VLLM_CONFIG',
2530
value: JSON.stringify({ url: '', model: '', enabled: false }),

src/seeders/tests/database.seeder.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ describe('DatabaseSeeders', () => {
6464
await seeder.run(em);
6565

6666
// APP_NAME, MAINTENANCE_MODE, MOODLE_SYNC_INTERVAL_MINUTES,
67-
// SENTIMENT_VLLM_CONFIG
67+
// MOODLE_SYNC_ENABLED, SENTIMENT_VLLM_CONFIG
6868
// eslint-disable-next-line @typescript-eslint/unbound-method
69-
expect(em.persist).toHaveBeenCalledTimes(4);
69+
expect(em.persist).toHaveBeenCalledTimes(5);
7070
});
7171

7272
it('should not seed duplicates for existing configurations', async () => {

0 commit comments

Comments
 (0)