update
All checks were successful
Deploy to Production / deploy (push) Successful in 23s

This commit is contained in:
silverpro89
2026-01-27 18:33:35 +07:00
parent 2c7b4675a7
commit 816794a861
14 changed files with 1827 additions and 3 deletions

View File

@@ -0,0 +1,188 @@
# Lesson Data Fill Worker
Worker này tự động xử lý các lesson trong database và tạo task lên BullMQ để xử lý dữ liệu.
## Chức năng
Worker sẽ:
1. Lấy các lesson từ database (chỉ lesson đã publish và có content_json)
2. Phân tích content_json để trích xuất:
- Danh sách vocabulary IDs
- Danh sách grammar IDs
3. Tạo task lên BullMQ queue `process-data` với label `process_data`
## Cài đặt
Worker sử dụng BullMQ và Redis đã được cấu hình sẵn trong project.
## Chạy Worker
### 1. Chạy worker trực tiếp:
```bash
node workers/lessonDataFillWorker.js
```
Worker sẽ chạy và lắng nghe jobs từ queue `lesson-data-fill`.
### 2. Trigger worker xử lý tất cả lessons:
```bash
# Xử lý tất cả lessons (mặc định batch size = 50)
node scripts/triggerLessonDataFill.js
# Xử lý với batch size tùy chỉnh
node scripts/triggerLessonDataFill.js --batch-size 100
# Xử lý giới hạn số lượng lessons
node scripts/triggerLessonDataFill.js --total 200
# Chỉ xử lý lessons có content_type cụ thể
node scripts/triggerLessonDataFill.js --type vocabulary
# Chỉ xử lý lessons trong một chapter
node scripts/triggerLessonDataFill.js --chapter <chapter-id>
# Kết hợp các options
node scripts/triggerLessonDataFill.js --batch-size 100 --total 500 --type grammar
```
## Cấu trúc dữ liệu
### Input (Lesson content_json)
```json
{
"type": "vocabulary",
"vocabulary_ids": ["uuid1", "uuid2", "uuid3"],
"exercises": [...]
}
```
hoặc
```json
{
"type": "grammar",
"grammar_ids": ["uuid1", "uuid2"],
"examples": [...],
"exercises": [...]
}
```
hoặc review lesson:
```json
{
"type": "review",
"sections": [
{
"type": "vocabulary",
"vocabulary_ids": ["uuid1", "uuid2"]
},
{
"type": "grammar",
"grammar_ids": ["uuid3"]
}
]
}
```
### Output (Task data gửi lên BullMQ)
```json
{
"type": "lesson_data",
"lesson": {
"lesson_id": "uuid",
"lesson_title": "Lesson Title",
"lesson_number": 1,
"lesson_content_type": "vocabulary",
"chapter_id": "uuid",
"chapter_title": "Chapter Title",
"vocabularies": ["vocab-uuid-1", "vocab-uuid-2"],
"grammars": ["grammar-uuid-1"]
},
"timestamp": "2026-01-27T10:30:00.000Z"
}
```
## Queue và Worker Configuration
- **Input Queue**: `lesson-data-fill`
- **Output Queue**: `process-data`
- **Job Label**: `process_data`
- **Concurrency**: 3 jobs đồng thời
- **Rate Limit**: 10 jobs/second
- **Retry**: 3 lần với exponential backoff
## Monitoring
Worker sẽ log các thông tin sau:
```
[LessonDataFillWorker] Starting job <job-id>
[LessonDataFillWorker] Found 50 lessons
[LessonDataFillWorker] Lesson <lesson-id>: 10 vocabularies
[LessonDataFillWorker] Lesson <lesson-id>: 5 grammars
[LessonDataFillWorker] ✅ Created task: <task-id> for lesson <lesson-id>
[LessonDataFillWorker] ✅ Created 45 process_data tasks
[LessonDataFillWorker] ✅ Job <job-id> completed
```
## Filters
Worker hỗ trợ các filter sau:
- `lesson_content_type`: Lọc theo loại nội dung (`vocabulary`, `grammar`, `phonics`, `review`, `mixed`)
- `chapter_id`: Lọc theo chapter cụ thể
- Luôn chỉ lấy lesson đã publish (`is_published = true`)
- Luôn chỉ lấy lesson có content_json (`lesson_type = 'json_content'`)
## Error Handling
- Worker sẽ tự động retry 3 lần khi gặp lỗi
- Failed jobs sẽ được giữ lại 7 ngày để review
- Completed jobs sẽ được giữ lại 24 giờ
## Integration
Để xử lý các task `process_data` được tạo ra, bạn cần tạo một worker khác để consume queue `process-data`:
```javascript
const { Worker } = require('bullmq');
const { connectionOptions } = require('../config/bullmq');
const processDataWorker = new Worker(
'process-data',
async (job) => {
const { lesson } = job.data;
// Xử lý dữ liệu lesson ở đây
console.log('Processing lesson:', lesson.lesson_id);
console.log('Vocabularies:', lesson.vocabularies);
console.log('Grammars:', lesson.grammars);
// TODO: Implement your processing logic
},
{
connection: connectionOptions,
prefix: process.env.BULLMQ_PREFIX || 'vcb',
}
);
```
## Graceful Shutdown
Worker hỗ trợ graceful shutdown khi nhận signal SIGTERM hoặc SIGINT:
```bash
# Dừng worker an toàn
Ctrl + C
```
Worker sẽ:
1. Dừng nhận job mới
2. Hoàn thành các job đang xử lý
3. Đóng kết nối Redis
4. Thoát process

View File

@@ -0,0 +1,282 @@
const { Worker, Queue } = require('bullmq');
const { connectionOptions } = require('../config/bullmq');
const models = require('../models');
const { Op } = require('sequelize');
/**
* Lesson Data Fill Worker
* Worker này lấy các lesson từ database và tạo task xử lý dữ liệu lên BullMQ
* - Liệt kê các từ vựng hoặc ngữ pháp của lesson
* - Ghi thành task lên BullMQ với label 'process_data'
*/
class LessonDataFillWorker {
constructor() {
// Queue để ghi task process_data
this.processDataQueue = new Queue('process-data', {
connection: connectionOptions,
prefix: process.env.BULLMQ_PREFIX || 'vcb',
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000,
},
removeOnComplete: {
age: 24 * 3600,
count: 1000,
},
removeOnFail: {
age: 7 * 24 * 3600,
},
},
});
// Worker để xử lý các lesson
this.worker = new Worker(
'lesson-data-fill',
async (job) => await this.processJob(job),
{
connection: connectionOptions,
prefix: process.env.BULLMQ_PREFIX || 'vcb',
concurrency: 3,
limiter: {
max: 10,
duration: 1000,
},
}
);
this.setupEventHandlers();
}
/**
* Process job - Lấy lessons và tạo task xử lý
*/
async processJob(job) {
console.log(`[LessonDataFillWorker] Starting job ${job.id}`);
try {
const { batchSize = 50, offset = 0, filters = {} } = job.data;
// Lấy lessons từ database
const lessons = await this.getLessons(batchSize, offset, filters);
console.log(`[LessonDataFillWorker] Found ${lessons.length} lessons`);
// Xử lý từng lesson
const tasks = [];
for (const lesson of lessons) {
const taskData = await this.extractLessonData(lesson);
if (taskData) {
// Tạo task lên BullMQ
const task = await this.createProcessDataTask(taskData);
tasks.push(task);
}
}
console.log(`[LessonDataFillWorker] ✅ Created ${tasks.length} process_data tasks`);
return {
success: true,
lessonsProcessed: lessons.length,
tasksCreated: tasks.length,
offset,
hasMore: lessons.length === batchSize,
};
} catch (error) {
console.error(`[LessonDataFillWorker] ❌ Error:`, error.message);
throw error;
}
}
/**
* Lấy lessons từ database
*/
async getLessons(limit, offset, filters) {
const whereClause = {
is_published: true,
lesson_type: 'json_content',
content_json: {
[Op.ne]: null,
},
};
// Thêm filters nếu có
if (filters.lesson_content_type) {
whereClause.lesson_content_type = filters.lesson_content_type;
}
if (filters.chapter_id) {
whereClause.chapter_id = filters.chapter_id;
}
const lessons = await models.Lesson.findAll({
where: whereClause,
limit,
offset,
order: [['created_at', 'DESC']],
include: [
{
model: models.Chapter,
as: 'chapter',
attributes: ['id', 'chapter_number', 'chapter_title', 'subject_id'],
},
],
});
return lessons;
}
/**
* Trích xuất dữ liệu từ lesson
*/
async extractLessonData(lesson) {
try {
const contentJson = lesson.content_json;
if (!contentJson || typeof contentJson !== 'object') {
return null;
}
const lessonData = {
lesson_id: lesson.id,
lesson_title: lesson.lesson_title,
lesson_number: lesson.lesson_number,
lesson_content_type: lesson.lesson_content_type,
chapter_id: lesson.chapter_id,
chapter_title: lesson.chapter?.chapter_title,
vocabularies: [],
grammars: [],
};
// Trích xuất vocabulary IDs
if (contentJson.vocabulary_ids && Array.isArray(contentJson.vocabulary_ids)) {
lessonData.vocabularies = contentJson.vocabulary_ids;
console.log(`[LessonDataFillWorker] Lesson ${lesson.id}: ${lessonData.vocabularies.length} vocabularies`);
}
// Trích xuất grammar IDs
if (contentJson.grammar_ids && Array.isArray(contentJson.grammar_ids)) {
lessonData.grammars = contentJson.grammar_ids;
console.log(`[LessonDataFillWorker] Lesson ${lesson.id}: ${lessonData.grammars.length} grammars`);
}
// Xử lý review lessons (có thể chứa cả vocab và grammar)
if (contentJson.sections && Array.isArray(contentJson.sections)) {
for (const section of contentJson.sections) {
if (section.vocabulary_ids && Array.isArray(section.vocabulary_ids)) {
lessonData.vocabularies.push(...section.vocabulary_ids);
}
if (section.grammar_ids && Array.isArray(section.grammar_ids)) {
lessonData.grammars.push(...section.grammar_ids);
}
}
// Loại bỏ duplicate IDs
lessonData.vocabularies = [...new Set(lessonData.vocabularies)];
lessonData.grammars = [...new Set(lessonData.grammars)];
}
// Chỉ return nếu có vocabulary hoặc grammar
if (lessonData.vocabularies.length > 0 || lessonData.grammars.length > 0) {
return lessonData;
}
return null;
} catch (error) {
console.error(`[LessonDataFillWorker] Error extracting lesson data:`, error.message);
return null;
}
}
/**
* Tạo task process_data lên BullMQ
*/
async createProcessDataTask(lessonData) {
try {
const job = await this.processDataQueue.add(
'process_data',
{
type: 'lesson_data',
lesson: lessonData,
timestamp: new Date().toISOString(),
},
{
priority: 5,
jobId: `lesson-${lessonData.lesson_id}-${Date.now()}`,
}
);
console.log(`[LessonDataFillWorker] ✅ Created task: ${job.id} for lesson ${lessonData.lesson_id}`);
return {
jobId: job.id,
lessonId: lessonData.lesson_id,
};
} catch (error) {
console.error(`[LessonDataFillWorker] ❌ Error creating task:`, error.message);
throw error;
}
}
/**
* Setup event handlers
*/
setupEventHandlers() {
this.worker.on('completed', (job) => {
console.log(`[LessonDataFillWorker] ✅ Job ${job.id} completed`);
});
this.worker.on('failed', (job, err) => {
console.error(`[LessonDataFillWorker] ❌ Job ${job?.id} failed:`, err.message);
});
this.worker.on('error', (err) => {
console.error(`[LessonDataFillWorker] ❌ Worker error:`, err.message);
});
this.worker.on('ready', () => {
console.log('[LessonDataFillWorker] 🚀 Worker ready');
});
this.processDataQueue.on('error', (err) => {
console.error(`[ProcessDataQueue] ❌ Queue error:`, err.message);
});
}
/**
* Graceful shutdown
*/
async close() {
console.log('[LessonDataFillWorker] Closing worker...');
await this.worker.close();
await this.processDataQueue.close();
console.log('[LessonDataFillWorker] Worker closed');
}
}
// Export class và instance để sử dụng
module.exports = LessonDataFillWorker;
// Nếu chạy trực tiếp file này
if (require.main === module) {
const worker = new LessonDataFillWorker();
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM received, closing worker...');
await worker.close();
process.exit(0);
});
process.on('SIGINT', async () => {
console.log('SIGINT received, closing worker...');
await worker.close();
process.exit(0);
});
console.log('🚀 Lesson Data Fill Worker started');
}

View File

@@ -0,0 +1,258 @@
const { Worker } = require('bullmq');
const { connectionOptions } = require('../config/bullmq');
const models = require('../models');
/**
* Process Data Worker
* Worker này xử lý các task từ queue 'process-data'
* - Nhận lesson data với danh sách vocabulary và grammar IDs
* - Thực hiện các xử lý nghiệp vụ với dữ liệu
*/
class ProcessDataWorker {
constructor() {
this.worker = new Worker(
'process-data',
async (job) => await this.processJob(job),
{
connection: connectionOptions,
prefix: process.env.BULLMQ_PREFIX || 'vcb',
concurrency: 5, // Process 5 jobs đồng thời
limiter: {
max: 20,
duration: 1000, // 20 jobs/second
},
}
);
this.setupEventHandlers();
}
/**
* Process job - Xử lý task process_data
*/
async processJob(job) {
const { type, lesson, timestamp } = job.data;
console.log(`[ProcessDataWorker] Processing job ${job.id} - Type: ${type}`);
console.log(`[ProcessDataWorker] Lesson: ${lesson.lesson_title} (${lesson.lesson_id})`);
try {
if (type === 'lesson_data') {
const result = await this.processLessonData(lesson);
console.log(`[ProcessDataWorker] ✅ Completed job ${job.id}`);
return result;
} else {
console.log(`[ProcessDataWorker] ⚠️ Unknown type: ${type}`);
return { success: false, error: 'Unknown type' };
}
} catch (error) {
console.error(`[ProcessDataWorker] ❌ Error processing job ${job.id}:`, error.message);
throw error;
}
}
/**
* Xử lý lesson data
*/
async processLessonData(lesson) {
const {
lesson_id,
lesson_title,
vocabularies,
grammars,
chapter_id,
} = lesson;
console.log(`[ProcessDataWorker] Processing lesson: ${lesson_title}`);
console.log(`[ProcessDataWorker] - Vocabularies: ${vocabularies.length}`);
console.log(`[ProcessDataWorker] - Grammars: ${grammars.length}`);
const result = {
lesson_id,
processed: {
vocabularies: 0,
grammars: 0,
},
details: {
vocabularies: [],
grammars: [],
},
};
// Xử lý vocabularies
if (vocabularies.length > 0) {
const vocabResult = await this.processVocabularies(vocabularies, lesson_id);
result.processed.vocabularies = vocabResult.count;
result.details.vocabularies = vocabResult.items;
}
// Xử lý grammars
if (grammars.length > 0) {
const grammarResult = await this.processGrammars(grammars, lesson_id);
result.processed.grammars = grammarResult.count;
result.details.grammars = grammarResult.items;
}
console.log(`[ProcessDataWorker] ✅ Processed: ${result.processed.vocabularies} vocabs, ${result.processed.grammars} grammars`);
return result;
}
/**
* Xử lý vocabularies
* TODO: Implement logic xử lý vocabularies theo yêu cầu nghiệp vụ
*/
async processVocabularies(vocabularyIds, lessonId) {
const items = [];
// Lấy thông tin vocabularies từ database
const vocabs = await models.Vocab.findAll({
where: {
vocab_id: vocabularyIds,
},
include: [
{
model: models.VocabMapping,
as: 'mappings',
},
{
model: models.VocabForm,
as: 'forms',
},
],
});
for (const vocab of vocabs) {
console.log(`[ProcessDataWorker] - Vocab: ${vocab.word} (${vocab.vocab_id})`);
// TODO: Thực hiện xử lý nghiệp vụ ở đây
// Ví dụ:
// - Cập nhật thống kê
// - Tạo cache
// - Sync với hệ thống khác
// - Tạo notification
// - Log audit
items.push({
vocab_id: vocab.vocab_id,
word: vocab.word,
word_type: vocab.word_type,
mappings_count: vocab.mappings?.length || 0,
forms_count: vocab.forms?.length || 0,
});
}
return {
count: vocabs.length,
items,
};
}
/**
* Xử lý grammars
* TODO: Implement logic xử lý grammars theo yêu cầu nghiệp vụ
*/
async processGrammars(grammarIds, lessonId) {
const items = [];
// Lấy thông tin grammars từ database
const grammars = await models.Grammar.findAll({
where: {
id: grammarIds,
},
include: [
{
model: models.GrammarMapping,
as: 'mappings',
},
],
});
for (const grammar of grammars) {
console.log(`[ProcessDataWorker] - Grammar: ${grammar.title} (${grammar.id})`);
// TODO: Thực hiện xử lý nghiệp vụ ở đây
// Ví dụ:
// - Cập nhật thống kê
// - Tạo cache
// - Sync với hệ thống khác
// - Tạo notification
// - Log audit
items.push({
grammar_id: grammar.id,
title: grammar.title,
grammar_type: grammar.grammar_type,
mappings_count: grammar.mappings?.length || 0,
});
}
return {
count: grammars.length,
items,
};
}
/**
* Setup event handlers
*/
setupEventHandlers() {
this.worker.on('completed', (job, returnvalue) => {
console.log(`[ProcessDataWorker] ✅ Job ${job.id} completed`);
});
this.worker.on('failed', (job, err) => {
console.error(`[ProcessDataWorker] ❌ Job ${job?.id} failed:`, err.message);
});
this.worker.on('error', (err) => {
console.error(`[ProcessDataWorker] ❌ Worker error:`, err.message);
});
this.worker.on('ready', () => {
console.log('[ProcessDataWorker] 🚀 Worker ready and listening for jobs...');
});
this.worker.on('active', (job) => {
console.log(`[ProcessDataWorker] 🔄 Processing job ${job.id}...`);
});
}
/**
* Graceful shutdown
*/
async close() {
console.log('[ProcessDataWorker] Closing worker...');
await this.worker.close();
console.log('[ProcessDataWorker] Worker closed');
}
}
// Export class
module.exports = ProcessDataWorker;
// Nếu chạy trực tiếp file này
if (require.main === module) {
const worker = new ProcessDataWorker();
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM received, closing worker...');
await worker.close();
process.exit(0);
});
process.on('SIGINT', async () => {
console.log('SIGINT received, closing worker...');
await worker.close();
process.exit(0);
});
console.log('═══════════════════════════════════════════════════════');
console.log(' Process Data Worker');
console.log(' Queue: process-data');
console.log(' Concurrency: 5');
console.log('═══════════════════════════════════════════════════════\n');
}