Skip to content

Commit 241f1ce

Browse files
authored
Merge pull request #5 from topcoder-platform/fix/event-based-transition-fixes
Event based transition fixes
2 parents 7fbe2e5 + c8d369c commit 241f1ce

File tree

12 files changed

+315
-100
lines changed

12 files changed

+315
-100
lines changed

pnpm-lock.yaml

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/autopilot/autopilot.module.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@ import { Module, forwardRef } from '@nestjs/common';
22
import { AutopilotService } from './services/autopilot.service';
33
import { KafkaModule } from '../kafka/kafka.module';
44
import { SchedulerService } from './services/scheduler.service';
5+
import { ScheduleModule } from '@nestjs/schedule';
6+
import { ChallengeModule } from '../challenge/challenge.module';
57

68
@Module({
7-
imports: [forwardRef(() => KafkaModule)],
9+
imports: [
10+
forwardRef(() => KafkaModule),
11+
ScheduleModule.forRoot(),
12+
ChallengeModule,
13+
],
814
providers: [AutopilotService, SchedulerService],
915
exports: [AutopilotService],
1016
})

src/autopilot/interfaces/autopilot.interface.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface PhaseTransitionPayload {
1313
operator: string;
1414
projectStatus: string;
1515
date?: string;
16+
challengeId?: number; // Added to meet requirement #6
1617
}
1718

1819
export interface ChallengeUpdatePayload {
@@ -21,16 +22,14 @@ export interface ChallengeUpdatePayload {
2122
status: string;
2223
operator: string;
2324
date?: string;
24-
phaseId?: number;
25-
phaseTypeName?: string;
2625
}
2726

2827
export interface CommandPayload {
2928
command: string;
3029
operator: string;
3130
projectId?: number;
3231
date?: string;
33-
phaseId?: number;
32+
phaseId?: number; // Keep this to support individual phase cancellation
3433
}
3534

3635
export interface PhaseTransitionMessage extends BaseMessage {

src/autopilot/services/autopilot.service.ts

Lines changed: 127 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
ChallengeUpdatePayload,
77
CommandPayload,
88
} from '../interfaces/autopilot.interface';
9+
import { ChallengeApiService } from '../../challenge/challenge-api.service';
10+
import { AUTOPILOT_COMMANDS } from '../../common/constants/commands.constants';
911

1012
@Injectable()
1113
export class AutopilotService {
@@ -14,7 +16,10 @@ export class AutopilotService {
1416
// Store active schedules for tracking purposes
1517
private activeSchedules = new Map<string, string>();
1618

17-
constructor(private readonly schedulerService: SchedulerService) {}
19+
constructor(
20+
private readonly schedulerService: SchedulerService,
21+
private readonly challengeApiService: ChallengeApiService,
22+
) {}
1823

1924
/**
2025
* Schedule a phase transition - HIGH LEVEL business logic
@@ -32,6 +37,16 @@ export class AutopilotService {
3237
this.activeSchedules.delete(phaseKey);
3338
}
3439

40+
// Check if date is in the past - if so, process immediately instead of throwing error
41+
const endTime = phaseData.date ? new Date(phaseData.date).getTime() : 0;
42+
if (endTime <= Date.now()) {
43+
this.logger.log(
44+
`Phase ${phaseKey} end time is in the past, processing immediately`,
45+
);
46+
void this.handlePhaseTransition(phaseData);
47+
return `immediate-${phaseKey}`;
48+
}
49+
3550
// Schedule new transition
3651
const jobId = this.schedulerService.schedulePhaseTransition(phaseData);
3752
this.activeSchedules.set(phaseKey, jobId);
@@ -75,12 +90,13 @@ export class AutopilotService {
7590
* Reschedule a phase transition - BUSINESS LOGIC operation
7691
* This is the high-level method that combines cancel + schedule with proper logging
7792
*/
78-
reschedulePhaseTransition(
93+
async reschedulePhaseTransition(
7994
projectId: number,
8095
newPhaseData: PhaseTransitionPayload,
81-
): string {
96+
): Promise<string> {
8297
const phaseKey = `${projectId}:${newPhaseData.phaseId}`;
8398
const existingJobId = this.activeSchedules.get(phaseKey);
99+
let wasRescheduled = false;
84100

85101
// Check if a job is already scheduled
86102
if (existingJobId) {
@@ -108,6 +124,7 @@ export class AutopilotService {
108124
this.logger.log(
109125
`Detected change in end time for phase ${phaseKey}, rescheduling.`,
110126
);
127+
wasRescheduled = true;
111128
}
112129

113130
// Cancel the previous job
@@ -117,9 +134,15 @@ export class AutopilotService {
117134
// Schedule the new transition
118135
const newJobId = this.schedulePhaseTransition(newPhaseData);
119136

120-
this.logger.log(
121-
`Successfully rescheduled phase ${newPhaseData.phaseId} with new end time: ${newPhaseData.date}`,
122-
);
137+
// Only log "rescheduled" if an existing job was actually rescheduled
138+
if (wasRescheduled) {
139+
this.logger.log(
140+
`Successfully rescheduled phase ${newPhaseData.phaseId} with new end time: ${newPhaseData.date}`,
141+
);
142+
}
143+
144+
// Add an await to satisfy the linter
145+
await Promise.resolve();
123146

124147
return newJobId;
125148
}
@@ -134,7 +157,7 @@ export class AutopilotService {
134157
);
135158
if (canceled) {
136159
this.logger.log(
137-
`Canceled scheduled transition for phase ${message.phaseId} (project ${message.projectId})`,
160+
`Removed job for phase ${message.phaseId} (project ${message.projectId}) from registry`,
138161
);
139162
}
140163
}
@@ -147,31 +170,57 @@ export class AutopilotService {
147170
/**
148171
* Handle challenge updates that might affect phase schedules
149172
*/
150-
handleChallengeUpdate(message: ChallengeUpdatePayload): void {
173+
async handleChallengeUpdate(message: ChallengeUpdatePayload): Promise<void> {
151174
this.logger.log(`Handling challenge update: ${JSON.stringify(message)}`);
152175

153-
if (!message.phaseId || !message.date) {
154-
this.logger.warn(
155-
`Skipping scheduling — challenge update missing required phase data.`,
176+
try {
177+
// Extract phaseId from message if available (for backward compatibility)
178+
// Cast to unknown first, then to Record to avoid type errors
179+
const anyMessage = message as unknown as Record<string, unknown>;
180+
const phaseId = anyMessage.phaseId as number | undefined;
181+
182+
if (!phaseId) {
183+
this.logger.warn(
184+
`Skipping scheduling — challenge update missing phase ID.`,
185+
);
186+
return;
187+
}
188+
189+
// Fetch phase details using the API service
190+
const phaseDetails = await this.challengeApiService.getPhaseDetails(
191+
message.projectId,
192+
phaseId,
156193
);
157-
return;
158-
}
159194

160-
const payload: PhaseTransitionPayload = {
161-
projectId: message.projectId,
162-
phaseId: message.phaseId,
163-
phaseTypeName: message.phaseTypeName || 'UNKNOWN', // placeholder
164-
operator: message.operator,
165-
projectStatus: message.status,
166-
date: message.date,
167-
state: 'END',
168-
};
195+
if (!phaseDetails) {
196+
this.logger.warn(
197+
`Skipping scheduling — could not fetch phase details for project ${message.projectId}, phase ${phaseId}.`,
198+
);
199+
return;
200+
}
169201

170-
this.logger.log(
171-
`Scheduling updated phase: ${message.projectId}:${message.phaseId}`,
172-
);
202+
const payload: PhaseTransitionPayload = {
203+
projectId: message.projectId,
204+
challengeId: message.challengeId, // Added to meet requirement #6
205+
phaseId: phaseId,
206+
phaseTypeName: phaseDetails.phaseTypeName,
207+
operator: message.operator,
208+
projectStatus: message.status,
209+
date: message.date || phaseDetails.date,
210+
state: 'END',
211+
};
173212

174-
this.reschedulePhaseTransition(message.projectId, payload);
213+
this.logger.log(
214+
`Scheduling updated phase: ${message.projectId}:${phaseId}`,
215+
);
216+
217+
await this.reschedulePhaseTransition(message.projectId, payload);
218+
} catch (error) {
219+
this.logger.error(
220+
`Error handling challenge update: ${error.message}`,
221+
error.stack,
222+
);
223+
}
175224
}
176225

177226
/**
@@ -184,38 +233,72 @@ export class AutopilotService {
184233

185234
try {
186235
switch (command.toLowerCase()) {
187-
case 'cancel_schedule':
236+
case AUTOPILOT_COMMANDS.CANCEL_SCHEDULE:
188237
if (!projectId) {
189-
this.logger.warn('cancel_schedule: missing projectId');
238+
this.logger.warn(
239+
`${AUTOPILOT_COMMANDS.CANCEL_SCHEDULE}: missing projectId`,
240+
);
190241
return;
191242
}
192243

193-
for (const key of this.activeSchedules.keys()) {
194-
if (key.startsWith(`${projectId}:`)) {
195-
this.cancelPhaseTransition(projectId, Number(key.split(':')[1]));
244+
// If phaseId is provided, cancel only that specific phase
245+
if (phaseId) {
246+
const canceled = this.cancelPhaseTransition(projectId, phaseId);
247+
if (canceled) {
248+
this.logger.log(
249+
`Canceled scheduled transition for phase ${projectId}:${phaseId}`,
250+
);
251+
} else {
252+
this.logger.warn(
253+
`No active schedule found for phase ${projectId}:${phaseId}`,
254+
);
255+
}
256+
} else {
257+
// Otherwise, cancel all phases for the project
258+
for (const key of this.activeSchedules.keys()) {
259+
if (key.startsWith(`${projectId}:`)) {
260+
const phaseIdFromKey = Number(key.split(':')[1]);
261+
this.cancelPhaseTransition(projectId, phaseIdFromKey);
262+
}
196263
}
197264
}
198265
break;
199266

200-
case 'reschedule_phase': {
267+
case AUTOPILOT_COMMANDS.RESCHEDULE_PHASE: {
201268
if (!projectId || !phaseId || !date) {
202269
this.logger.warn(
203-
`reschedule_phase: missing required data (projectId, phaseId, or date)`,
270+
`${AUTOPILOT_COMMANDS.RESCHEDULE_PHASE}: missing required data (projectId, phaseId, or date)`,
204271
);
205272
return;
206273
}
207274

208-
const payload: PhaseTransitionPayload = {
209-
projectId,
210-
phaseId,
211-
phaseTypeName: 'UNKNOWN',
212-
operator,
213-
state: 'END',
214-
projectStatus: 'IN_PROGRESS',
215-
date,
216-
};
217-
218-
this.reschedulePhaseTransition(projectId, payload);
275+
// Fetch phase type name using the API service
276+
void (async () => {
277+
try {
278+
const phaseTypeName =
279+
await this.challengeApiService.getPhaseTypeName(
280+
projectId,
281+
phaseId,
282+
);
283+
284+
const payload: PhaseTransitionPayload = {
285+
projectId,
286+
phaseId,
287+
phaseTypeName,
288+
operator,
289+
state: 'END',
290+
projectStatus: 'IN_PROGRESS',
291+
date,
292+
};
293+
294+
await this.reschedulePhaseTransition(projectId, payload);
295+
} catch (error) {
296+
this.logger.error(
297+
`Error in reschedule_phase command: ${error.message}`,
298+
error.stack,
299+
);
300+
}
301+
})();
219302
break;
220303
}
221304

0 commit comments

Comments
 (0)