4.2 · Bootstrap + 6 DB connections
บทนี้เจาะ index.js (bootstrap) และ connector.js (6 Sequelize instances) เพื่ออธิบายว่า service เริ่มทำงานอย่างไรตอน boot ทำไมมี DB 6 ตัวทั้งที่ audit บอกแค่ 2 และ cron ทำงานอย่างไรใน deployment 3 replicas
Bootstrap sequence
index.js เรียงการทำงานตอน boot ตามนี้
1. import modules (config, connector, models, typeDefs, resolvers, ...)
├── redis client connect ที่ module load time (lib/redis.js:11)
└── cron jobs register ที่ module load time (cron.js)
2. new ApolloServer({ typeDefs, resolvers, schemaDirectives, context, ... })
3. const app = express()
app.disable('x-powered-by')
4. app.use(bodyParser.json())
app.use(routes) ← REST endpoints mount (signin/signout/token/conicle)
5. server.applyMiddleware({ app, path, bodyParserConfig })
6. app.listen({ port: APP_PORT }, async () => {
checkAdAuthenConnection() ← one-shot LDAP bind เพื่อ verify connectivity
redisConnection() ← attach connect/error listeners (no-op ถ้า connected แล้ว)
jobs ← cron jobs (registered แล้ว ตัวแปรนี้เป็น array ว่างใน non-APP_NO=1)
db.authenticate()
.then(syncModelToTable) ← ★ auto-create tables ใน prod!
.catch(log)
dbHr.authenticate() ← fire-and-forget
.then(...)
.catch(log)
})โดยทั่วไปจะเริ่ม debug "ทำไม boot ช้า" ที่ checkAdAuthenConnection() ก่อน — ถ้า AD down หรือ slow, boot จะ hang ที่นี่
Apollo + Express wiring
const server = new ApolloServer({
typeDefs,
resolvers,
schemaDirectives,
context: async ({ req, res }) => {ใช้ apollo-server-express (Apollo v2 — ปัจจุบัน EOL, modernize เป็น @apollo/server v4)
Playground / introspection / debug
introspection: !IS_PRODUCTION,
playground: !IS_PRODUCTION,
debug: !IS_PRODUCTION,ทั้งสาม gate ด้วย IS_PRODUCTION — ต่างจาก vtrc-api ที่ hardcode true (บท 9.10) ตรงนี้ถือว่าทำถูกกว่า
มี defense-in-depth อีกชั้นใน resolver
export default {
Query: {
__type () {
throw new Error('You cannot make introspection')
},
__schema () {
console.log('OKOKOKOK')
throw new Error('You cannot make introspection')
}
},ที่น่าสนใจ — __schema resolver มี console.log('OKOKOKOK') debug print ที่ลืมลบ และจะ fire ถ้า introspection ผ่านมาถึง resolver (rare เพราะ Apollo block ก่อน)
CORS
ไม่มี CORS middleware ใน codebase (grep confirms 0 hits ของ cors) — CORS (ถ้ามี) อยู่ที่ nginx proxy นอก Node
Port + path
app.listen({ port: APP_PORT }, async () => {
console.log(`*** -- START WITH APP NUMBER #${APP_NO} -- ***`)
console.log(`${new Date().toLocaleString()} | 🚀 Server ready at -> http://localhost:${APP_PORT}${server.graphqlPath}`)| Config | Default | Source |
|---|---|---|
APP_PORT | 8080 | config.js:20 |
APP_NO | '1' | config.js — selects which cron jobs run |
API_GRAPHQL_PATH | /api/graphql | config.js:8 |
Latent bug ใน Docker
EXPOSE 9000Dockerfile expose 9000 แต่ runtime ฟัง APP_PORT (default 8080) ส่วน docker-compose.yml:53 map 8000:8000 โดยไม่ set APP_PORT env — ทำให้ container ฟัง 8080 แต่ host map 8000 mismatch
ในทางปฏิบัติ env injection ภายนอก compose file อาจ set ให้ตรง แต่เป็น pattern ที่ fragile
6 DB connections — ความจริงที่ audit พลาด
audit 02-architecture-map.md:62 บอกว่า cu-central-api เปิด db + dbHr — แต่แท้จริงแล้วมี 6 Sequelize instances
| # | Variable | Schema env | ใช้ทำอะไร | Export | Auth ตอน boot |
|---|---|---|---|---|---|
| 1 | db | DB_NAME | core tables (account, otp, verifycode, ...) | ✅ | ✅ |
| 2 | dbHr | DB_HR_NAME | HRMI employee + organization (ใหญ่สุด) | ✅ | ✅ |
| 3 | dbHrTemp | DB_HR_DATA_LAKE_NAME | staging tables (taxes, address, phone) | ✅ | ❌ |
| 4 | dbMasDb | DB_MASTER_DATA_NAME | master data | ✅ | ❌ |
| 5 | dbHRMI_test | hardcoded "dbHRMI_test" | dead (ไม่มี model ผูก) | ✅ | ❌ |
| 6 | dbHRMI_Center_HIS | DB_HRMI_CENTER_HIS | heal-card discount + log | ✅ | ❌ |
ทั้ง 6 ใช้ DB_USERNAME/DB_PASSWORD/DB_HOST/DB_PORT เดียวกัน — เปลี่ยน database name เท่านั้น
dialectOptions: {
options: {
requestTimeout: 30000,
useUTC: false,
dateStrings: true,
typeCast: true,
timezone: TIME_ZONE
}
},driver คือ tedious (package.json:42 — version ^9.2.1 ต่ำกว่า current 18+)
Pool sizing
| Connection | pool.max |
|---|---|
db | 100 |
dbHr | 100 |
dbHrTemp | 50 |
dbMasDb | 100 |
dbHRMI_test | 100 |
dbHRMI_Center_HIS | 100 |
pool.max: 100 × 5 active connections × 3 replicas = 1,500 MSSQL sessions ใน worst case — ทำให้ MSSQL connection limit หมดเร็ว โดยเฉพาะถ้าแชร์ instance กับ centralize-api (ที่มี connection ของตัวเอง)
โดยทั่วไปจะเริ่ม debug "MSSQL connection timeout" ที่นี่ก่อน — ลด pool.max หรือเพิ่ม MSSQL max_connections
dbHRMI_test — dead weight ที่เปลือง resource
const dbHRMI_test = new Sequelize("dbHRMI_test", DB_USERNAME, DB_PASSWORD, {
host: DB_HOST,
port: DB_PORT,
dialect: DB_SCHEMA,
// ...
})// walk(path.join(__dirname, 'hrmi_test'))
// .filter(...)
// .forEach(file => {
// const model = require(...).default(dbHRMI_test, ...)
// Models[model.name] = model
// })- connection สร้างขึ้นทุก boot (เปิด pool ที่ไม่มีใครใช้)
- model loader commented out — ไม่มี model ผูก
- ผล — pool 100 session เปล่าที่กิน MSSQL resource
Fix: ลบทั้ง connection และ loader comment ออกจาก codebase
Model loader และ connection mapping
models/index.js เป็น recursive loader ที่กระจาย model ไปยัง connection ตาม subdirectory
// [CORE] Recursive import Model from files
walk(path.join(__dirname, 'core'))
.filter(file => ...)
.forEach(file => {
const model = require(...).default(db, Sequelize.DataTypes, Sequelize)
Models[model.name] = model
})
// [HRMI] Recursive import Model from files
walk(path.join(__dirname, 'hrmi'))
.forEach(file => {
const model = require(...).default(dbHr, Sequelize.DataTypes, Sequelize)
Models[model.name] = model
})
// [HRMI_TEMP] ...hrRawData → dbHrTemp
// [MASTER_DATA] ...masterData → dbMasDb
// hrmiHis → dbHRMI_Center_HISสรุป mapping
| Subdir | Connection | Schema |
|---|---|---|
models/core/ | db | CENTRALIZE_APP |
models/hrmi/ | dbHr | dbHRMI_Center |
models/hrRawData/ | dbHrTemp | CENTRALIZE_HR |
models/masterData/ | dbMasDb | CENTRALIZE_MASTER_DATA |
models/hrmiHis/ | dbHRMI_Center_HIS | dbHRMI_Center_HIS |
models/hrmi_test/ | dbHRMI_test | (dead — commented out) |
Global DATE monkey-patch
Sequelize.DATE.prototype._stringify = function (date, options) {
// บังคับ format YYYY-MM-DD HH:mm:ss.SSS สำหรับทุก DATE field ทุก connection
}force date format ให้ทุก connection เหมือนกัน — เป็น hack เพราะ MSSQL/Sequelize บางครั้ง format ไม่ตรงกัน ถือเป็น code smell ที่บ่งบอกว่าทีมไม่ได้คิดเรื่อง type handling ตอน design
syncModelToTable whitelist
const syncModels = [
'VerifyCode', 'createdAccounts', 'PhoneNumber', 'Otp', 'MasterOrgSystem',
'HrTaxes', 'HrAddress', 'HrPhoneNumber', 'UserIdsMapping',
'HealCardDiscount', 'HealCardDiscountLog', 'HrFundBenefitPerson'
]เหมือน vtrc-api — auto-create tables จาก model definition โดยใช้ sync({ force: false }) ใน prod
- 12 models ที่ auto-create (ลิสต์ hardcoded)
- นอกเหนือจาก 12 ตัวนี้ ต้องมี table อยู่แล้วใน MSSQL
ปัญหาเดียวกับ vtrc-api (CORR-6)
db.authenticate()
.then(() => syncModelToTable())
.then(() => console.log('syncModelToTable done'))
.catch(err => log.error(...))auto-sync ใน prod เป็น risk — model change deploy โดย accident จะ alter prod schema โดยไม่มี migration review
Fix: gate ด้วย IS_PRODUCTION
if (!IS_PRODUCTION) {
await syncModelToTable()
}Default data seeding — bug เดียวกับ vtrc-api
// ใน lib/model.js syncModelToTable()
await Models[modelName].sync({ force: false }).then(synced => {
if (typeof defaultData[modelName] !== 'undefined') {
Models[modelName].findAll({ limit: 1 }).then(rows => {
if (rows.length === 0) {
Models[modelName].bulkCreate(defaultData[modelName]).then(...)
}
})
}
})findAll+bulkCreatechain เป็น fire-and-forget (ไม่ await) — race condition ถ้า boot หลาย replica พร้อมกัน- ทำให้ seeding อาจทำงานซ้อนกันใน deployment 3 replicas
Cron jobs + APP_NO gating
import schedule from 'node-schedule'
import { APP_NO } from './config'
import * as cronMethods from './lib/cronMethods'
import * as log from './lib/wlog'
const jobs = []
if (+APP_NO === 1) {
// Every day at 03:30.
jobs.push(schedule.scheduleJob('30 3 * * *', async () => {
await cronMethods.updateUserId()
}))
// Every day at 03:45.
jobs.push(schedule.scheduleJob('40 3 * * *', async () => {
await cronMethods.conicleTransferLongTerm())
}))
jobs.push(schedule.scheduleJob('8 * * * *', async () => {
console.log("In cron funciton.")
await cronMethods.hisTransferLongTerm()
}))
}
export default jobs3 jobs ที่ run เฉพาะ replica ที่ APP_NO === 1
| Schedule | Method | หน้าที่ |
|---|---|---|
30 3 * * * (daily 03:30) | updateUserId() | rebuild userIdsMapping table |
40 3 * * * (daily 03:40) | conicleTransferLongTerm() | SFTP push ไป Conicle |
8 * * * * (hourly :08) | hisTransferLongTerm() | pull heal-card จาก HIS API |
Comments drift + typo
| คอมเมนต์ | ความจริง |
|---|---|
| "Every day at 03:45" | แท้จริงแล้วคือ 40 3 * * * = 03:40 (ต่างกัน 5 นาที) |
console.log("In cron funciton.") | typo "funciton" — debug print ลืมลบ |
HA weakness
node-schedule เป็น in-process scheduler — ถ้า replica APP_NO=1 restart กลางคัน job หายไป ไม่มี retry ไม่มี visibility
Fix: ใช้ distributed scheduler (BullMQ + Redis หรือ Go asynq) — detail ใน Volume 15
สรุปการ boot
Module load:
├── Redis client connect (sync)
└── Cron jobs register (ถ้า APP_NO=1)
app.listen callback:
├── checkAdAuthenConnection() ★ อาจ hang ถ้า AD down
├── redisConnection() (no-op ถ้า connected)
├── jobs (empty array ถ้า APP_NO≠1)
├── db.authenticate()
│ └── syncModelToTable() ★ auto-create ใน prod (CORR-6)
└── dbHr.authenticate() (fire-and-forget)Total boot time ปกติ ~3-5 วินาที — ขึ้นกับ AD + MSSQL responsiveness
อ่านต่อ → 4.3 Authentication + LDAP/AD flow