5 · Cron jobs
pms-api ใช้ @nestjs/schedule ^6.0.0 (package.json) เปิดด้วย ScheduleModule.forRoot() ที่ pms-api/src/app.module.ts (branch: uat):104
พบ @Cron decorator 3 จุด — 2 จุด active และ 1 จุดถูก comment ปิด (method ยังอยู่ในไฟล์แต่ไม่ถูก schedule)
ทุก claim อ้างจาก canonical branch uat
สรุปรายการ cron
| # | Job | Cron expression | Timezone | สถานะ | ไฟล์ |
|---|---|---|---|---|---|
| 1 | Daily period notifications | '0 6 * * *' (06:00 ทุกวัน) | Asia/Bangkok | Active | configNotification.controller.ts:30-41 |
| 2 | Workforce trigger sync | '*/1 * * * *' (ทุก 1 นาที) | default server TZ | Active | usagePermission.service.ts:1928-1936 |
| 3 | Daily data migration | เคยเป็น '0 6 * * *' / test '*/10 * * * * *' | (comment) | Inactive | migrateData.controller.ts:15-29 |
Job 1 — Daily notification check (handleCronNotifications)
Schedule และ entry point
pms-api/src/modules/configNotification/configNotification.controller.ts (branch: uat):27-41
// @Cron('*/10 * * * * *', { // leftover test schedule
@Cron('0 6 * * *', {
timeZone: 'Asia/Bangkok',
})
async handleCronNotifications() {
console.log('Starting daily notification check at 6 AM Thailand time');
try {
this.emailService.checkAndSendNotifications();
console.log('Daily notification check completed');
} catch (error) {
console.error('Error during daily notification check:', error);
}
}Code trace
06:00 Asia/Bangkok
→ ConfigNotificationController.handleCronNotifications()
→ ConfigNotificationSendEmailService.checkAndSendNotifications()
→ repo.getCurrentPeriodWithNotifications()
→ for each ConfigNotification where isEmail=true
→ switch(notiTypeCode) map period date + EJS template
→ sendPeriodNotificationEmail()
→ getNotificationRecipients()
→ EmailService.sendEmail() (batch 500)
→ insert NotifyEntity logNotification types (7 ชนิดใน switch)
จาก configNotificationSendEmail.service.ts (branch: uat):81-129
notiTypeCode | วันที่อ้างอิงจาก period | EJS template | Subject (ย่อ) |
|---|---|---|---|
notiGoalStart | settingGoalStart | period/period-goal-start.ejs | แจ้งรอบกำหนดตัวชี้วัด |
notiGoalSubmit | settingGoalEnd | period/period-goal-submit.ejs | ใกล้ถึงวันส่งตัวชี้วัด |
notiCheckData | checkDataStart | period/period-check-data.ejs | ตรวจสอบข้อมูลผู้รับการประเมิน |
notiUpdateResult | updateResultStart | period/period-update-result.ejs | บันทึกผลงานที่ทำได้จริง |
notiEvaluationStart | evaluationStart | period/period-evaluation-start.ejs | เริ่มประเมินผล |
notiEvaluationEnd | evaluationEnd | period/period-evaluation-end.ejs | ส่งการประเมิน |
notiFeedback | feedbackEnd | period/period-feedback.ejs | แจ้งผลประเมิน |
เอกสารเก่าบางฉบับพูดถึง 8 ประเภท (รวม approval) — ใน switch ปัจจุบันมี 7 case + default: continue ไม่มี notiApproval ในโค้ดนี้
Advance notification — ถ้า isAdvanceNoti และ advanceNotiDay > 0 จะถอยวันที่ด้วย calculateNotificationDate() (:44-54)
Recipients และ email batch
sendPeriodNotificationEmail (:142-200)
- เลือกผู้รับตาม flag
isEvaluatee/isEvaluator/isApprover/isOrgUnitAdmin - แบ่ง batch ละ 500 อีเมล (
batchSize = 500) - template data ใส่
backofficeUrl: process.env.VTRC_ESS_URL(บรรทัด 172) — ลิงก์กลับเข้า ESS - วันที่ในอีเมลแปลงเป็น พ.ศ. ผ่าน
getThaiDateFullMonth()
Failure modes — Job 1
| Failure mode | พฤติกรรมจริง | ผลกระทบ |
|---|---|---|
ไม่ await checkAndSendNotifications() | fire-and-forget — catch ของ controller จับได้แค่ sync throw | error ภายใน async ไปที่ service logger แทน; log "completed" อาจพิมพ์ก่อนงานจบ |
| Date gate ถูก comment | บรรทัด :133 — if (process.env.NODE_ENV === 'development' || targetDate /* isSameDate ... */) | นอก development ถ้า targetDate มีค่า (truthy Date) จะส่ง ทุกประเภทที่มีวันที่ตั้งไว้ ทุกวัน ไม่ใช่เฉพาะวันที่ตรงกับวันนี้ — bug ความถูกต้องสำคัญ |
| SMTP ล่ม | EmailService retry 3 ครั้งแล้ว fail; controller console.error อย่างเดียว | ผู้ใช้ไม่ได้อีเมล; ไม่มี alert ไป ops |
| ไม่มี period / config | service log แล้ว return | เงียบ — ถูกต้องตาม logic |
| ไม่มี recipients | log แล้ว skip ประเภทนั้น | เงียบ |
| Multi-instance | ไม่มี distributed lock | ทุก replica ส่งอีเมลซ้ำเวลา 06:00 (UNVERIFIED จำนวน replica) |
console.log ใน prod | main.ts no-op เฉพาะ console.log ไม่ปิด console.error | error ยังเห็นใน stdout; success log หายใน prod |
Manual trigger
POST /configNotification/check-notifications เป็น @Public() แต่ทำงานเฉพาะ NODE_ENV === 'development' (controller.ts:44-55) — บน UAT คืน { message: 'Nope !!!' }
Job 2 — Workforce trigger sync (handleCron)
Schedule และ entry point
pms-api/src/modules/usagePermission/usagePermission.service.ts (branch: uat):1928-1936
// ตรวจสอบข้อมูลที่ update มาจาก workforce ทุกๆ 1 นาที เพื่อ update ข้อมูลให้กับ pms
@Cron('*/1 * * * *')
async handleCron() {
try {
await this.getEmployeeTriggerQeue();
} catch (error) {
this.logger.error(error);
}
}หมายเหตุ — ไม่ระบุ timeZone จึงใช้ timezone ของ process (Dockerfile ตั้ง TZ=Asia/Bangkok ที่บรรทัด 5 — สอดคล้องถ้า deploy ตาม Dockerfile)
Code trace
ทุก 1 นาที
→ UsagePermissionService.handleCron()
→ getEmployeeTriggerQeue() // :1698
→ repo.getEmployeeQueue() // workforce: tbEmployeeTriggerQueue where isSent=0 and isFail=0
→ sort empCode ASC, createdAt DESC
→ dedupe ตาม identityCard (+ dataType สำหรับประเภทที่อนุญาตซ้ำ)
→ sort ตาม dataTypePriority (resign ก่อน … promote ท้าย)
→ for each latestEmployees:
SendEmailPromote / Transfer / TransferAndPromote → sendEmailPromoteTransfer()
CancelTransfer → updateCancelTransfer()
insert → processNewPMS()
else → processUpdatePMS() // promote, transfer_DB, resign, …
→ mark queue rows isSent / isFailQueue source
pms-api/src/modules/usagePermission/usagePermission.repositories.ts (branch: uat):507-516
public async getEmployeeQueue(): Promise<EmpTriggerQueueDto[]> {
const query = this.workforceEntityManager
.createQueryBuilder().from('tbEmployeeTriggerQueue', 'emp')
.where('emp.isSent = 0 and emp.isFail = 0')
.orderBy('emp.createdAt', 'ASC');
const result = await query.getRawMany();
return result as EmpTriggerQueueDto[];
}อ่านจาก workforce connection โดยตรง — ไม่ผ่าน HTTP
dataType ที่รองรับ (จาก dedupe / priority)
getEmployeeTriggerQeue บรรทัด :1713-1750
| dataType | อนุญาต duplicate ในรอบเดียว | Priority (เลขน้อย = ก่อน) | Handler หลัก |
|---|---|---|---|
resign | ใช่ | 1 | processUpdatePMS |
CancelTransfer | ใช่ | 2 | updateCancelTransfer |
SendEmailPromote | ใช่ | 3 | sendEmailPromoteTransfer |
SendEmailTransfer | ใช่ | 4 | sendEmailPromoteTransfer |
TransferAndPromote | ใช่ | 5 | sendEmailPromoteTransfer |
transferOrgunit | ใช่ | 6 | processUpdatePMS |
transfer_DB | ใช่ | 7 | processUpdatePMS |
promote | ใช่ | 8 | processUpdatePMS |
insert | ไม่ (key = identityCard) | 999 (default) | processNewPMS |
| อื่น ๆ | ไม่ | 999 | processUpdatePMS |
service ไฟล์นี้ยาว 1,937 บรรทัด — logic sync กระจายอยู่หลาย method (processUpdatePMS, processNewPMS, email promote ฯลฯ) เป็นจุดที่แก้แล้วพังง่าย
Failure modes — Job 2
| Failure mode | พฤติกรรมจริง | ผลกระทบ |
|---|---|---|
| Overlapping execution | @nestjs/schedule ไม่กัน job ซ้อน; ไม่มี mutex ในโค้ด | ถ้า getEmployeeTriggerQeue > 1 นาที → 2 job แข่งกัน mark queue / อัปเดต PMSUser → race / duplicate email |
| Multi-instance | ไม่มี distributed lock | replica ทุกตัวดึง queue ชุดเดียวกันพร้อมกัน |
| Queue query ล้ม | getEmployeeQueue catch แล้ว return null | NPE/skip ตอน .slice() ถ้าไม่เช็ค null — ขึ้นกับ runtime (ควรตรวจ) |
| Partial failure กลาง loop | error ต่อคนอาจถูกกลืนใน handler ย่อย | บางคน sync สำเร็จ บางคนค้าง isSent=0 |
| Silent top-level catch | this.logger.error(error) อย่างเดียว | ไม่มี alert ภายนอก |
| Load สูงตอน promote หมู่ | sort + loop แบบ sequential | รอบถัดไปเริ่มก่อนจบ → ซ้อน (ดูด้านบน) |
Manual trigger ที่เกี่ยวข้อง
POST /usagePermission/send-email-promote-new (@Public() + NODE_ENV === 'development') — ทดสอบส่งอีเมล promote/transfer โดยไม่รอ queue
Job 3 — Data migration (INACTIVE)
pms-api/src/modules/migrateData/migrateData.controller.ts (branch: uat):14-29
// @Cron('*/10 * * * * *', {
// @Cron('0 6 * * *', {
// timeZone: 'Asia/Bangkok',
// })
async handleCronMigration() {
// await this.service.migrateAllData();
}@Cronถูก comment ทั้งคู่ — Nest จะ ไม่ register เป็น scheduled job- method
handleCronMigrationไม่ถูกเรียกจากที่อื่นในโค้ด (dead schedule path) - migration ทำผ่าน manual
POST /migrateData/migrate-dataเท่านั้น และจำกัดNODE_ENV === 'development' - service จริงอยู่ที่
migrateData.service.ts(~631 บรรทัด) —migrateAllData(buCode?, empCode?)
ถ้า uncomment @Cron โดยไม่ใส่ distributed lock จะชนกับความเสี่ยงเดียวกับ Job 1 (อีเมล/migrate ซ้ำทุก instance)
เอกสารภายในที่ไม่ตรงกับโค้ด
pms-api/docs/PMS-Period-Notification-System.md (branch: uat) ระบุว่า:
| ข้อในเอกสาร | ความจริงในโค้ด |
|---|---|
cron อยู่ที่ period.controller.ts | อยู่ที่ configNotification.controller.ts |
| cron ถูก comment ต้อง uncomment เอง | Job 1 active แล้ว (@Cron('0 6 * * *')) |
| ขั้นตอน enable ใน README ของ docs | ทำให้ engineer เข้าใจผิดว่าระบบแจ้งเตือนยังไม่ทำงาน |
จัดเป็น QUAL-PMS-01 ใน 06 Scorecard
สิ่งที่ไม่มีในทุก cron ของ pms-api
- Distributed lock / leader election (Redis, DB lock, k8s CronJob แยก)
- Overlap guard (
@Cron+ semaphore /isRunningflag) - Ops alert เมื่อ fail (Slack/email ทีม) — มีแค่ logger / console
- Metrics (duration, success count, queue depth)
- Idempotency key ระดับอีเมลรายวัน (นอกจาก
NotifyEntitylog ภายหลังส่ง)
ตารางเปรียบเทียบกับ benefit-api
| หัวข้อ | pms-api | benefit-api |
|---|---|---|
@nestjs/schedule version | ^6.0.0 | ^2.2.0 |
| Active crons | 2 | (ดู benefit ch05) |
| รอบถี่สุด | ทุก 1 นาที | — |
| Distributed lock | ไม่มี | ไม่มี (ปัญหาเดียวกัน) |
| Error alert | ไม่มี | ไม่มี |
แนวทาง monitor ตอนปฏิบัติการ
- ดูขนาด
tbEmployeeTriggerQueueที่isSent=0 AND isFail=0— ถ้าโตต่อเนื่อง cron sync มีปัญหา - วัดเวลา
getEmployeeTriggerQeue— ถ้า median > 45s เสี่ยง overlap - ตรวจ
tbNotifyหลัง 06:00 — ถ้ามี notify ซ้ำทุกประเภททุกวัน สอดคล้องกับ date-gate bug ที่ Job 1 - อย่าเชื่อ
PMS-Period-Notification-System.mdจนกว่าจะอัปเดตให้ตรงโค้ด
ไป บท 6 Scorecard →