This commit is contained in:
77
.gitea/workflows/deploy.yml
Normal file
77
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,77 @@
|
||||
name: Deploy to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Deploy to server
|
||||
run: |
|
||||
echo "🚀 Deploying to /var/www/services/sena_db_api"
|
||||
echo "📁 Current directory: $(pwd)"
|
||||
echo "📁 GITHUB_WORKSPACE: $GITHUB_WORKSPACE"
|
||||
ls -la
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
mkdir -p /var/www/services/sena_db_api
|
||||
|
||||
# Copy files to destination (runner đã mount /var/www)
|
||||
rsync -av --delete \
|
||||
--exclude 'node_modules' \
|
||||
--exclude '.git' \
|
||||
--exclude '.gitea' \
|
||||
--exclude 'logs' \
|
||||
--exclude 'uploads' \
|
||||
--exclude 'runner-data' \
|
||||
--exclude '.env' \
|
||||
--exclude 'docker-compose.runner.yml' \
|
||||
--exclude 'setup-runner.sh' \
|
||||
$GITHUB_WORKSPACE/ /var/www/services/sena_db_api/
|
||||
|
||||
echo "✅ Files copied successfully"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd /var/www/services/sena_db_api
|
||||
echo "📦 Installing dependencies with pnpm..."
|
||||
pnpm install --production
|
||||
echo "✅ Dependencies installed"
|
||||
|
||||
- name: Create required directories
|
||||
run: |
|
||||
cd /var/www/services/sena_db_api
|
||||
mkdir -p logs uploads data
|
||||
echo "✅ Directories created"
|
||||
|
||||
- name: Restart PM2 service
|
||||
run: |
|
||||
echo "🔄 Restarting PM2 service..."
|
||||
|
||||
# Tạo trigger file để PM2 watcher restart service
|
||||
touch /var/www/services/sena_db_api/.pm2-restart-trigger
|
||||
|
||||
# Đợi watcher xử lý (tối đa 10s)
|
||||
for i in {1..5}; do
|
||||
if [ ! -f /var/www/services/sena_db_api/.pm2-restart-trigger ]; then
|
||||
echo "✅ PM2 service restarted"
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "⚠️ Trigger file created, PM2 will restart shortly"
|
||||
|
||||
- name: Deployment completed
|
||||
run: |
|
||||
echo "✅ Deployment completed successfully!"
|
||||
echo "📍 Location: /var/www/services/sena_db_api"
|
||||
echo "🔧 PM2 service will restart within 2 seconds"
|
||||
211
PM2_GUIDE.md
Normal file
211
PM2_GUIDE.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# PM2 Process Management
|
||||
|
||||
PM2 script để quản lý tất cả các processes của Sena School Management System.
|
||||
|
||||
## Cài đặt PM2
|
||||
|
||||
```bash
|
||||
npm install -g pm2
|
||||
```
|
||||
|
||||
## Processes
|
||||
|
||||
System bao gồm các processes sau:
|
||||
|
||||
1. **sena-api** - Main API Server (2 instances, cluster mode)
|
||||
2. **worker-db-write** - Database Write Worker (1 instance)
|
||||
3. **worker-lesson-fill** - Lesson Data Fill Worker (1 instance)
|
||||
4. **worker-process-data** - Process Data Worker (2 instances)
|
||||
|
||||
## Commands
|
||||
|
||||
### Quản lý tất cả processes
|
||||
|
||||
```bash
|
||||
# Start tất cả processes
|
||||
npm run pm2:start
|
||||
|
||||
# Stop tất cả processes
|
||||
npm run pm2:stop
|
||||
|
||||
# Restart tất cả processes
|
||||
npm run pm2:restart
|
||||
|
||||
# Delete tất cả processes
|
||||
npm run pm2:delete
|
||||
|
||||
# Xem status của tất cả processes
|
||||
npm run pm2:status
|
||||
```
|
||||
|
||||
### Quản lý từng nhóm
|
||||
|
||||
```bash
|
||||
# Chỉ start API server
|
||||
npm run pm2:start:api
|
||||
|
||||
# Chỉ start workers
|
||||
npm run pm2:start:workers
|
||||
```
|
||||
|
||||
### Logs & Monitoring
|
||||
|
||||
```bash
|
||||
# Xem tất cả logs
|
||||
npm run pm2:logs
|
||||
|
||||
# Xem logs của một process cụ thể
|
||||
pm2 logs sena-api
|
||||
pm2 logs worker-process-data
|
||||
|
||||
# Xóa tất cả logs
|
||||
npm run pm2:flush
|
||||
|
||||
# Mở monitor dashboard
|
||||
npm run pm2:monit
|
||||
```
|
||||
|
||||
### Auto-start khi boot
|
||||
|
||||
```bash
|
||||
# Cấu hình PM2 khởi động cùng hệ thống
|
||||
npm run pm2:startup
|
||||
|
||||
# Sau đó chạy command mà PM2 hiển thị (với sudo nếu cần)
|
||||
# Rồi save danh sách processes
|
||||
npm run pm2:save
|
||||
```
|
||||
|
||||
### Commands nâng cao
|
||||
|
||||
```bash
|
||||
# Restart một process cụ thể
|
||||
pm2 restart sena-api
|
||||
|
||||
# Stop một process cụ thể
|
||||
pm2 stop worker-lesson-fill
|
||||
|
||||
# Xem thông tin chi tiết
|
||||
pm2 describe sena-api
|
||||
|
||||
# Xem metrics
|
||||
pm2 monit
|
||||
|
||||
# Scale API instances
|
||||
pm2 scale sena-api 4
|
||||
```
|
||||
|
||||
## File cấu hình
|
||||
|
||||
### ecosystem.config.js
|
||||
|
||||
File cấu hình PM2 chính, định nghĩa:
|
||||
- Tên processes
|
||||
- Script path
|
||||
- Số lượng instances
|
||||
- Environment variables
|
||||
- Log files
|
||||
- Memory limits
|
||||
- Restart policies
|
||||
|
||||
### Log Files
|
||||
|
||||
Logs được lưu trong thư mục `logs/`:
|
||||
|
||||
```
|
||||
logs/
|
||||
├── api-error.log
|
||||
├── api-out.log
|
||||
├── worker-db-write-error.log
|
||||
├── worker-db-write-out.log
|
||||
├── worker-lesson-fill-error.log
|
||||
├── worker-lesson-fill-out.log
|
||||
├── worker-process-data-error.log
|
||||
└── worker-process-data-out.log
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### 1. Install PM2 globally
|
||||
```bash
|
||||
npm install -g pm2
|
||||
```
|
||||
|
||||
### 2. Start all processes
|
||||
```bash
|
||||
npm run pm2:start
|
||||
```
|
||||
|
||||
### 3. Save process list
|
||||
```bash
|
||||
npm run pm2:save
|
||||
```
|
||||
|
||||
### 4. Configure auto-start
|
||||
```bash
|
||||
npm run pm2:startup
|
||||
# Chạy command được hiển thị
|
||||
npm run pm2:save
|
||||
```
|
||||
|
||||
### 5. Monitor
|
||||
```bash
|
||||
npm run pm2:status
|
||||
npm run pm2:logs
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Process không start
|
||||
```bash
|
||||
# Xem logs để biết lỗi
|
||||
pm2 logs <process-name>
|
||||
|
||||
# Xem thông tin chi tiết
|
||||
pm2 describe <process-name>
|
||||
```
|
||||
|
||||
### Memory leak
|
||||
```bash
|
||||
# Processes tự động restart khi vượt quá giới hạn memory
|
||||
# API: 1GB
|
||||
# Workers: 512MB
|
||||
```
|
||||
|
||||
### Restart processes sau khi update code
|
||||
```bash
|
||||
npm run pm2:restart
|
||||
```
|
||||
|
||||
### Xóa và start lại từ đầu
|
||||
```bash
|
||||
npm run pm2:delete
|
||||
npm run pm2:start
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Thay đổi environment trong [ecosystem.config.js](ecosystem.config.js):
|
||||
|
||||
```javascript
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3000,
|
||||
},
|
||||
env_development: {
|
||||
NODE_ENV: 'development',
|
||||
PORT: 3000,
|
||||
}
|
||||
```
|
||||
|
||||
Chạy với environment cụ thể:
|
||||
```bash
|
||||
pm2 start ecosystem.config.js --env development
|
||||
pm2 start ecosystem.config.js --env production
|
||||
```
|
||||
|
||||
## Help
|
||||
|
||||
```bash
|
||||
npm run pm2:help
|
||||
```
|
||||
@@ -31,7 +31,7 @@
|
||||
"keyPrefix": "sena:"
|
||||
},
|
||||
"server": {
|
||||
"port" : 3000,
|
||||
"port" : 10000,
|
||||
"env": "production"
|
||||
},
|
||||
"cors": {
|
||||
|
||||
136
data/family/g1/unit4.json
Normal file
136
data/family/g1/unit4.json
Normal file
@@ -0,0 +1,136 @@
|
||||
[
|
||||
{
|
||||
"chapter_id": "uuid_chapter_4_grade1",
|
||||
"lesson_number": 1,
|
||||
"lesson_title": "Lesson 1: Animals",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Learn animal words and use them in a chant",
|
||||
"lesson_content_type": "vocabulary",
|
||||
"content_json": {
|
||||
"type": "vocabulary",
|
||||
"learning_objectives": [
|
||||
"to learn animal words",
|
||||
"to use animal words in the form of a chant"
|
||||
],
|
||||
"vocabulary": ["bird", "bear", "hippo", "crocodile", "tiger"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_4_grade1",
|
||||
"lesson_number": 2,
|
||||
"lesson_title": "Lesson 2: What are they?",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Ask and answer about animals",
|
||||
"lesson_content_type": "grammar",
|
||||
"content_json": {
|
||||
"type": "grammar",
|
||||
"learning_objectives": [
|
||||
"to ask and answer 'What are they? They're (bears)'",
|
||||
"to recognize plurals with 's'",
|
||||
"to sing a song"
|
||||
],
|
||||
"grammar": "What are they? They're bears."
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_4_grade1",
|
||||
"lesson_number": 3,
|
||||
"lesson_title": "Lesson 3: Letters J & K",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Recognize letters J and K and their sounds",
|
||||
"lesson_content_type": "phonics",
|
||||
"content_json": {
|
||||
"type": "phonics",
|
||||
"learning_objectives": [
|
||||
"to recognize the upper- and lowercase forms of the letters j and k",
|
||||
"to associate them with the sounds /dʒ/ and /k/",
|
||||
"to pronounce the sounds /dʒ/ and /k/",
|
||||
"to be familiar with the names of the letters j and k"
|
||||
],
|
||||
"letters": ["J", "K"],
|
||||
"sounds": ["/dʒ/", "/k/"],
|
||||
"vocabulary": ["jug", "juice", "kangaroo", "key"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_4_grade1",
|
||||
"lesson_number": 4,
|
||||
"lesson_title": "Lesson 4: Numbers 9 and 10",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Learn and use numbers 9 and 10",
|
||||
"lesson_content_type": "vocabulary",
|
||||
"content_json": {
|
||||
"type": "vocabulary",
|
||||
"learning_objectives": [
|
||||
"to recognize and say the numbers 9 and 10",
|
||||
"to use the numbers 9 and 10 in the context of a song"
|
||||
],
|
||||
"vocabulary": ["nine", "ten"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_4_grade1",
|
||||
"lesson_number": 5,
|
||||
"lesson_title": "Lesson 5: Letters L & M",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Recognize letters L and M and their sounds",
|
||||
"lesson_content_type": "phonics",
|
||||
"content_json": {
|
||||
"type": "phonics",
|
||||
"learning_objectives": [
|
||||
"to recognize the upper- and lowercase forms of the letters l and m",
|
||||
"to associate them with the sounds /l/ and /m/",
|
||||
"to pronounce the sounds /l/ and /m/",
|
||||
"to be familiar with the names of the letters l and m"
|
||||
],
|
||||
"letters": ["L", "M"],
|
||||
"sounds": ["/l/", "/m/"],
|
||||
"vocabulary": ["lion", "lollipop", "man", "mango"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_4_grade1",
|
||||
"lesson_number": 6,
|
||||
"lesson_title": "Lesson 6: Story and Values",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Understand a short story and learn values",
|
||||
"lesson_content_type": "review",
|
||||
"content_json": {
|
||||
"type": "review",
|
||||
"learning_objectives": [
|
||||
"to recognize and identify words",
|
||||
"to understand a short story",
|
||||
"to review and consolidate language introduced in the unit"
|
||||
],
|
||||
"values": ["Be kind to animals"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_4_grade1",
|
||||
"lesson_number": 7,
|
||||
"lesson_title": "Culture: Children's Day",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Learn about Children's Day in Japan",
|
||||
"lesson_content_type": "vocabulary",
|
||||
"content_json": {
|
||||
"type": "vocabulary",
|
||||
"learning_objectives": ["to learn about Children's Day in Japan"],
|
||||
"vocabulary": ["fish", "bird", "flower", "frog"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_4_grade1",
|
||||
"lesson_number": 8,
|
||||
"lesson_title": "Consolidation",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Review and test Unit 4",
|
||||
"lesson_content_type": "review",
|
||||
"content_json": {
|
||||
"type": "review",
|
||||
"learning_objectives": [
|
||||
"to review and consolidate language introduced in the unit",
|
||||
"to do Unit Test"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
139
data/family/g1/unit5.json
Normal file
139
data/family/g1/unit5.json
Normal file
@@ -0,0 +1,139 @@
|
||||
[
|
||||
{
|
||||
"chapter_id": "uuid_chapter_5_grade1",
|
||||
"lesson_number": 1,
|
||||
"lesson_title": "Lesson 1: Body Parts",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Identify different parts of the body",
|
||||
"lesson_content_type": "vocabulary",
|
||||
"content_json": {
|
||||
"type": "vocabulary",
|
||||
"learning_objectives": [
|
||||
"to identify different parts of the body",
|
||||
"to use the words in the context of a chant"
|
||||
],
|
||||
"vocabulary": ["arms", "nose", "face", "legs", "fingers", "hands"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_5_grade1",
|
||||
"lesson_number": 2,
|
||||
"lesson_title": "Lesson 2: This and These",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Use this and these in sentences",
|
||||
"lesson_content_type": "grammar",
|
||||
"content_json": {
|
||||
"type": "grammar",
|
||||
"learning_objectives": [
|
||||
"to say sentences with 'this' and 'these'",
|
||||
"to complete sentences with 'this' and 'these'",
|
||||
"to recognize the difference between singular and plural forms of nouns"
|
||||
],
|
||||
"grammar": "This is.... These are...."
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_5_grade1",
|
||||
"lesson_number": 3,
|
||||
"lesson_title": "Lesson 3: Letters A, B, C, D",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Learn letters A, B, C, D and their sounds",
|
||||
"lesson_content_type": "phonics",
|
||||
"content_json": {
|
||||
"type": "phonics",
|
||||
"learning_objectives": [
|
||||
"to learn the names of the letters a, b, c, d",
|
||||
"to review the upper- and lowercase forms of a, b, c, d",
|
||||
"to associate them with their corresponding sounds",
|
||||
"to pronounce the sounds /b/ and /d/ at the ends of words"
|
||||
],
|
||||
"letters": ["A", "B", "C", "D"],
|
||||
"sounds": ["/b/", "/d/"],
|
||||
"vocabulary": ["a", "b", "c", "d", "e", "f", "g", "h"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_5_grade1",
|
||||
"lesson_number": 4,
|
||||
"lesson_title": "Lesson 4: Numbers 1-5",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Review numbers 1-5",
|
||||
"lesson_content_type": "vocabulary",
|
||||
"content_json": {
|
||||
"type": "vocabulary",
|
||||
"learning_objectives": [
|
||||
"to review the numbers 1-5",
|
||||
"to associate the numbers 1-5 with the words one to five"
|
||||
],
|
||||
"vocabulary": ["one", "two", "three", "four", "five"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_5_grade1",
|
||||
"lesson_number": 5,
|
||||
"lesson_title": "Lesson 5: Letters E, F, G, H",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Learn letters E, F, G, H and their sounds",
|
||||
"lesson_content_type": "phonics",
|
||||
"content_json": {
|
||||
"type": "phonics",
|
||||
"learning_objectives": [
|
||||
"to learn the names of the letters Ee, Ff, Gg, and Hh",
|
||||
"to review the upper- and lowercase forms of e, f, g, and h",
|
||||
"to associate them with their corresponding sounds",
|
||||
"to pronounce the sounds /f/ and /g/ at the ends of words"
|
||||
],
|
||||
"letters": ["E", "F", "G", "H"],
|
||||
"sounds": ["/f/", "/g/"],
|
||||
"vocabulary": ["b", "d", "f", "g"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_5_grade1",
|
||||
"lesson_number": 6,
|
||||
"lesson_title": "Lesson 6: Story and Values",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Understand a short story and learn values",
|
||||
"lesson_content_type": "review",
|
||||
"content_json": {
|
||||
"type": "review",
|
||||
"learning_objectives": [
|
||||
"to recognize and identify words",
|
||||
"to understand a short story",
|
||||
"to review and consolidate language introduced in the unit"
|
||||
],
|
||||
"values": ["Take care in the sun"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_5_grade1",
|
||||
"lesson_number": 7,
|
||||
"lesson_title": "Culture: Fruits in Cambodia",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Learn about fruits found in Cambodia",
|
||||
"lesson_content_type": "vocabulary",
|
||||
"content_json": {
|
||||
"type": "vocabulary",
|
||||
"learning_objectives": [
|
||||
"to learn some fruit found in Cambodia",
|
||||
"to design a menu"
|
||||
],
|
||||
"vocabulary": ["mango", "papaya", "pineapple", "watermelon"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_5_grade1",
|
||||
"lesson_number": 8,
|
||||
"lesson_title": "Consolidation",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Review and test Unit 5",
|
||||
"lesson_content_type": "review",
|
||||
"content_json": {
|
||||
"type": "review",
|
||||
"learning_objectives": [
|
||||
"to review and consolidate language introduced in the unit",
|
||||
"to do Unit Test"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
140
data/family/g1/unit6.json
Normal file
140
data/family/g1/unit6.json
Normal file
@@ -0,0 +1,140 @@
|
||||
[
|
||||
{
|
||||
"chapter_id": "uuid_chapter_6_grade1",
|
||||
"lesson_number": 1,
|
||||
"lesson_title": "Lesson 1: My Lunchbox",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Identify different foods in a lunchbox",
|
||||
"lesson_content_type": "vocabulary",
|
||||
"content_json": {
|
||||
"type": "vocabulary",
|
||||
"learning_objectives": [
|
||||
"to identify different foods in a lunchbox",
|
||||
"to use the words in the context of a chant"
|
||||
],
|
||||
"vocabulary": ["lunchbox", "sandwich", "drink", "banana", "cookie", "pear"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_6_grade1",
|
||||
"lesson_number": 2,
|
||||
"lesson_title": "Lesson 2: I have...",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Make sentences with have",
|
||||
"lesson_content_type": "grammar",
|
||||
"content_json": {
|
||||
"type": "grammar",
|
||||
"learning_objectives": [
|
||||
"to make sentences with 'have'",
|
||||
"to recognize difference between 'a' and 'an'"
|
||||
],
|
||||
"grammar": "I have a pear. I have my lunchbox."
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_6_grade1",
|
||||
"lesson_number": 3,
|
||||
"lesson_title": "Lesson 3: Letters I, J, K, L, M",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Learn letters I, J, K, L, M and their sounds",
|
||||
"lesson_content_type": "phonics",
|
||||
"content_json": {
|
||||
"type": "phonics",
|
||||
"learning_objectives": [
|
||||
"to learn the names of the letters i, j, k, l, m",
|
||||
"to review the upper- and lowercase forms of the letters",
|
||||
"to associate them with their corresponding sounds",
|
||||
"to pronounce the sounds /ɪ/, /ʤ/, /k/, /l/, /m/ at the beginning of words",
|
||||
"to pronounce the sounds /k/, /l/, /m/ at the end of words"
|
||||
],
|
||||
"letters": ["I", "J", "K", "L", "M"],
|
||||
"sounds": ["/ɪ/", "/ʤ/", "/k/", "/l/", "/m/"],
|
||||
"vocabulary": ["b", "d", "f", "g", "I", "j", "k", "l", "m"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_6_grade1",
|
||||
"lesson_number": 4,
|
||||
"lesson_title": "Lesson 4: Numbers 6-10",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Review numbers 6-10",
|
||||
"lesson_content_type": "vocabulary",
|
||||
"content_json": {
|
||||
"type": "vocabulary",
|
||||
"learning_objectives": [
|
||||
"to review the numbers 6-10",
|
||||
"to associate the numbers 6-10 with the words six to ten"
|
||||
],
|
||||
"vocabulary": ["six", "seven", "eight", "nine", "ten"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_6_grade1",
|
||||
"lesson_number": 5,
|
||||
"lesson_title": "Lesson 5: Review Letters A-M",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Review letters A to M",
|
||||
"lesson_content_type": "phonics",
|
||||
"content_json": {
|
||||
"type": "phonics",
|
||||
"learning_objectives": [
|
||||
"to review the names of the letters learned so far (a to m)",
|
||||
"to review the upper- and lowercase forms of the letters (a to m)",
|
||||
"to associate them with their corresponding sounds",
|
||||
"to recognize letters at the ends/beginnings of words",
|
||||
"to pronounce their sounds appropriately (/b/, /d/, /f/, /g/, /k/, /l/, /m/)"
|
||||
],
|
||||
"letters": ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M"],
|
||||
"sounds": ["/b/", "/d/", "/f/", "/g/", "/k/", "/l/", "/m/"],
|
||||
"vocabulary": ["b", "d", "f", "g", "k", "l", "m"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_6_grade1",
|
||||
"lesson_number": 6,
|
||||
"lesson_title": "Lesson 6: Story and Values",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Understand a short story and learn values",
|
||||
"lesson_content_type": "review",
|
||||
"content_json": {
|
||||
"type": "review",
|
||||
"learning_objectives": [
|
||||
"to recognize and identify words",
|
||||
"to understand a short story",
|
||||
"to review and consolidate language introduced in the unit"
|
||||
],
|
||||
"values": ["Share with others"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_6_grade1",
|
||||
"lesson_number": 7,
|
||||
"lesson_title": "Culture: Bondi Beach in Australia",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Learn about Bondi Beach in Australia",
|
||||
"lesson_content_type": "vocabulary",
|
||||
"content_json": {
|
||||
"type": "vocabulary",
|
||||
"learning_objectives": [
|
||||
"to learn some words about a beach in Australia",
|
||||
"to read a postcard"
|
||||
],
|
||||
"vocabulary": ["sand", "sea", "shell", "sun"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"chapter_id": "uuid_chapter_6_grade1",
|
||||
"lesson_number": 8,
|
||||
"lesson_title": "Consolidation",
|
||||
"lesson_type": "json_content",
|
||||
"lesson_description": "Review and test Unit 6",
|
||||
"lesson_content_type": "review",
|
||||
"content_json": {
|
||||
"type": "review",
|
||||
"learning_objectives": [
|
||||
"to review and consolidate language introduced in the unit",
|
||||
"to do Unit Test"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
102
ecosystem.config.js
Normal file
102
ecosystem.config.js
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* PM2 Ecosystem Configuration
|
||||
* Quản lý các process của Sena School Management System
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
// Main API Server
|
||||
{
|
||||
name: 'sena-api',
|
||||
script: './server.js',
|
||||
instances: 2, // Chạy 2 instances để tận dụng CPU
|
||||
exec_mode: 'cluster',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3000,
|
||||
},
|
||||
env_development: {
|
||||
NODE_ENV: 'development',
|
||||
PORT: 3000,
|
||||
},
|
||||
error_file: './logs/api-error.log',
|
||||
out_file: './logs/api-out.log',
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
||||
merge_logs: true,
|
||||
max_memory_restart: '1G',
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_restarts: 10,
|
||||
min_uptime: '10s',
|
||||
},
|
||||
|
||||
// Database Write Worker
|
||||
{
|
||||
name: 'worker-db-write',
|
||||
script: './workers/databaseWriteWorker.js',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
env_development: {
|
||||
NODE_ENV: 'development',
|
||||
},
|
||||
error_file: './logs/worker-db-write-error.log',
|
||||
out_file: './logs/worker-db-write-out.log',
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
||||
merge_logs: true,
|
||||
max_memory_restart: '512M',
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_restarts: 10,
|
||||
min_uptime: '10s',
|
||||
},
|
||||
|
||||
// Lesson Data Fill Worker
|
||||
{
|
||||
name: 'worker-lesson-fill',
|
||||
script: './workers/lessonDataFillWorker.js',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
env_development: {
|
||||
NODE_ENV: 'development',
|
||||
},
|
||||
error_file: './logs/worker-lesson-fill-error.log',
|
||||
out_file: './logs/worker-lesson-fill-out.log',
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
||||
merge_logs: true,
|
||||
max_memory_restart: '512M',
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_restarts: 10,
|
||||
min_uptime: '10s',
|
||||
},
|
||||
|
||||
// Process Data Worker
|
||||
{
|
||||
name: 'worker-process-data',
|
||||
script: './workers/processDataWorker.js',
|
||||
instances: 2, // Chạy 2 instances để xử lý song song
|
||||
exec_mode: 'fork',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
env_development: {
|
||||
NODE_ENV: 'development',
|
||||
},
|
||||
error_file: './logs/worker-process-data-error.log',
|
||||
out_file: './logs/worker-process-data-out.log',
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
||||
merge_logs: true,
|
||||
max_memory_restart: '512M',
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_restarts: 10,
|
||||
min_uptime: '10s',
|
||||
},
|
||||
],
|
||||
};
|
||||
15
package.json
15
package.json
@@ -7,7 +7,20 @@
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"test": "jest --coverage",
|
||||
"lint": "eslint ."
|
||||
"lint": "eslint .",
|
||||
"pm2:start": "node scripts/pm2.js start",
|
||||
"pm2:stop": "node scripts/pm2.js stop",
|
||||
"pm2:restart": "node scripts/pm2.js restart",
|
||||
"pm2:delete": "node scripts/pm2.js delete",
|
||||
"pm2:status": "node scripts/pm2.js status",
|
||||
"pm2:logs": "node scripts/pm2.js logs",
|
||||
"pm2:monit": "node scripts/pm2.js monit",
|
||||
"pm2:flush": "node scripts/pm2.js flush",
|
||||
"pm2:save": "node scripts/pm2.js save",
|
||||
"pm2:startup": "node scripts/pm2.js startup",
|
||||
"pm2:start:api": "node scripts/pm2.js start:api",
|
||||
"pm2:start:workers": "node scripts/pm2.js start:workers",
|
||||
"pm2:help": "node scripts/pm2.js help"
|
||||
},
|
||||
"keywords": [
|
||||
"expressjs",
|
||||
|
||||
@@ -158,7 +158,7 @@ router.post('/', authenticateToken, vocabController.createVocab);
|
||||
* 500:
|
||||
* description: Server error
|
||||
*/
|
||||
router.get('/', authenticateToken, vocabController.getAllVocabs);
|
||||
router.get('/', vocabController.getAllVocabs);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
|
||||
161
scripts/pm2.js
Normal file
161
scripts/pm2.js
Normal file
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* PM2 Management Script
|
||||
* Helper script để quản lý các PM2 processes
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const ECOSYSTEM_FILE = path.join(__dirname, '..', 'ecosystem.config.js');
|
||||
|
||||
// Colors for console output
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
red: '\x1b[31m',
|
||||
};
|
||||
|
||||
const log = (message, color = 'reset') => {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`);
|
||||
};
|
||||
|
||||
const exec = (command, options = {}) => {
|
||||
try {
|
||||
const result = execSync(command, {
|
||||
stdio: 'inherit',
|
||||
...options,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
log(`Error executing command: ${command}`, 'red');
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Commands
|
||||
const commands = {
|
||||
start: () => {
|
||||
log('\n🚀 Starting all processes...', 'blue');
|
||||
exec(`pm2 start ${ECOSYSTEM_FILE}`);
|
||||
log('✅ All processes started!', 'green');
|
||||
},
|
||||
|
||||
stop: () => {
|
||||
log('\n⏸️ Stopping all processes...', 'yellow');
|
||||
exec('pm2 stop ecosystem.config.js');
|
||||
log('✅ All processes stopped!', 'green');
|
||||
},
|
||||
|
||||
restart: () => {
|
||||
log('\n🔄 Restarting all processes...', 'blue');
|
||||
exec(`pm2 restart ${ECOSYSTEM_FILE}`);
|
||||
log('✅ All processes restarted!', 'green');
|
||||
},
|
||||
|
||||
delete: () => {
|
||||
log('\n🗑️ Deleting all processes...', 'yellow');
|
||||
exec('pm2 delete ecosystem.config.js');
|
||||
log('✅ All processes deleted!', 'green');
|
||||
},
|
||||
|
||||
status: () => {
|
||||
log('\n📊 Process status:', 'blue');
|
||||
exec('pm2 list');
|
||||
},
|
||||
|
||||
logs: (processName) => {
|
||||
if (processName) {
|
||||
log(`\n📋 Showing logs for ${processName}...`, 'blue');
|
||||
exec(`pm2 logs ${processName}`);
|
||||
} else {
|
||||
log('\n📋 Showing all logs...', 'blue');
|
||||
exec('pm2 logs');
|
||||
}
|
||||
},
|
||||
|
||||
monit: () => {
|
||||
log('\n📈 Opening process monitor...', 'blue');
|
||||
exec('pm2 monit');
|
||||
},
|
||||
|
||||
flush: () => {
|
||||
log('\n🧹 Flushing all logs...', 'yellow');
|
||||
exec('pm2 flush');
|
||||
log('✅ Logs flushed!', 'green');
|
||||
},
|
||||
|
||||
save: () => {
|
||||
log('\n💾 Saving PM2 process list...', 'blue');
|
||||
exec('pm2 save');
|
||||
log('✅ Process list saved!', 'green');
|
||||
},
|
||||
|
||||
startup: () => {
|
||||
log('\n⚙️ Configuring PM2 to start on system boot...', 'blue');
|
||||
exec('pm2 startup');
|
||||
log('✅ Run the command above, then run: npm run pm2:save', 'yellow');
|
||||
},
|
||||
|
||||
'start:api': () => {
|
||||
log('\n🚀 Starting API server only...', 'blue');
|
||||
exec(`pm2 start ${ECOSYSTEM_FILE} --only sena-api`);
|
||||
log('✅ API server started!', 'green');
|
||||
},
|
||||
|
||||
'start:workers': () => {
|
||||
log('\n🚀 Starting all workers...', 'blue');
|
||||
exec(`pm2 start ${ECOSYSTEM_FILE} --only worker-db-write,worker-lesson-fill,worker-process-data`);
|
||||
log('✅ All workers started!', 'green');
|
||||
},
|
||||
|
||||
help: () => {
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════════════════════════╗
|
||||
║ Sena PM2 Management Commands ║
|
||||
╠════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ npm run pm2:start - Start all processes ║
|
||||
║ npm run pm2:stop - Stop all processes ║
|
||||
║ npm run pm2:restart - Restart all processes ║
|
||||
║ npm run pm2:delete - Delete all processes ║
|
||||
║ npm run pm2:status - Show process status ║
|
||||
║ npm run pm2:logs [name] - Show logs (optional: process) ║
|
||||
║ npm run pm2:monit - Open process monitor ║
|
||||
║ npm run pm2:flush - Flush all logs ║
|
||||
║ npm run pm2:save - Save process list ║
|
||||
║ npm run pm2:startup - Configure startup script ║
|
||||
║ ║
|
||||
║ npm run pm2:start:api - Start API server only ║
|
||||
║ npm run pm2:start:workers - Start all workers only ║
|
||||
║ ║
|
||||
╠════════════════════════════════════════════════════════════════╣
|
||||
║ Process Names: ║
|
||||
║ - sena-api (Main API Server) ║
|
||||
║ - worker-db-write (Database Write Worker) ║
|
||||
║ - worker-lesson-fill (Lesson Data Fill Worker) ║
|
||||
║ - worker-process-data (Process Data Worker) ║
|
||||
╚════════════════════════════════════════════════════════════════╝
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
// Parse command
|
||||
const command = process.argv[2];
|
||||
const arg = process.argv[3];
|
||||
|
||||
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
||||
commands.help();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (commands[command]) {
|
||||
commands[command](arg);
|
||||
} else {
|
||||
log(`Unknown command: ${command}`, 'red');
|
||||
commands.help();
|
||||
process.exit(1);
|
||||
}
|
||||
117
scripts/triggerLessonDataFill.js
Normal file
117
scripts/triggerLessonDataFill.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Script để trigger Lesson Data Fill Worker
|
||||
* Chạy script này để bắt đầu xử lý tất cả lessons và tạo process_data tasks
|
||||
*/
|
||||
|
||||
const { Queue } = require('bullmq');
|
||||
const { connectionOptions } = require('../config/bullmq');
|
||||
|
||||
/**
|
||||
* Trigger worker để xử lý lessons
|
||||
*/
|
||||
async function triggerLessonDataFill(options = {}) {
|
||||
const {
|
||||
batchSize = 50,
|
||||
totalLessons = null, // null = process all
|
||||
filters = {}, // { lesson_content_type: 'vocabulary', chapter_id: 'xxx' }
|
||||
} = options;
|
||||
|
||||
const queue = new Queue('lesson-data-fill', {
|
||||
connection: connectionOptions,
|
||||
prefix: process.env.BULLMQ_PREFIX || 'vcb',
|
||||
});
|
||||
|
||||
try {
|
||||
let offset = 0;
|
||||
let jobsCreated = 0;
|
||||
let hasMore = true;
|
||||
|
||||
console.log('🚀 Starting lesson data fill process...');
|
||||
console.log('📋 Config:', { batchSize, totalLessons, filters });
|
||||
|
||||
while (hasMore) {
|
||||
// Kiểm tra nếu đã đạt giới hạn
|
||||
if (totalLessons && offset >= totalLessons) {
|
||||
console.log(`✅ Reached limit of ${totalLessons} lessons`);
|
||||
break;
|
||||
}
|
||||
|
||||
// Tạo job cho batch này
|
||||
const job = await queue.add(
|
||||
`process-lessons-batch-${offset}`,
|
||||
{
|
||||
batchSize,
|
||||
offset,
|
||||
filters,
|
||||
},
|
||||
{
|
||||
priority: 5,
|
||||
jobId: `lesson-fill-${offset}-${Date.now()}`,
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`✅ Created job ${job.id} for offset ${offset}`);
|
||||
jobsCreated++;
|
||||
|
||||
// Chờ một chút để tránh quá tải
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
offset += batchSize;
|
||||
|
||||
// Nếu không có totalLessons, sẽ dừng sau 1 job để kiểm tra hasMore
|
||||
if (!totalLessons) {
|
||||
// Worker sẽ báo hasMore trong result
|
||||
hasMore = false; // Tạm dừng, có thể chạy lại script để tiếp tục
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ Process completed!`);
|
||||
console.log(`📊 Total jobs created: ${jobsCreated}`);
|
||||
console.log(`📊 Lessons range: 0 to ${offset}`);
|
||||
|
||||
await queue.close();
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error.message);
|
||||
await queue.close();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const options = {};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg === '--batch-size' && args[i + 1]) {
|
||||
options.batchSize = parseInt(args[i + 1]);
|
||||
i++;
|
||||
}
|
||||
|
||||
if (arg === '--total' && args[i + 1]) {
|
||||
options.totalLessons = parseInt(args[i + 1]);
|
||||
i++;
|
||||
}
|
||||
|
||||
if (arg === '--type' && args[i + 1]) {
|
||||
options.filters = options.filters || {};
|
||||
options.filters.lesson_content_type = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
|
||||
if (arg === '--chapter' && args[i + 1]) {
|
||||
options.filters = options.filters || {};
|
||||
options.filters.chapter_id = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Run
|
||||
console.log('═══════════════════════════════════════════════════════');
|
||||
console.log(' Lesson Data Fill Trigger Script');
|
||||
console.log('═══════════════════════════════════════════════════════\n');
|
||||
|
||||
triggerLessonDataFill(options);
|
||||
188
workers/README_LESSON_DATA_FILL.md
Normal file
188
workers/README_LESSON_DATA_FILL.md
Normal 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
|
||||
282
workers/lessonDataFillWorker.js
Normal file
282
workers/lessonDataFillWorker.js
Normal 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');
|
||||
}
|
||||
258
workers/processDataWorker.js
Normal file
258
workers/processDataWorker.js
Normal 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');
|
||||
}
|
||||
Reference in New Issue
Block a user