3.2.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)
└── cron jobs register ที่ module load time (cron.js)
2. if (!IS_PRODUCTION) testScript() ← dev-only scratch runner
3. const app = express()
app.disable('x-powered-by')
4. routes(app, express) ← REST endpoints mount (signin/signout/token/conicle)
5. const server = new ApolloServer({ typeDefs, resolvers, schemaDirectives, context, ... })
6. server.applyMiddleware({ app, path, bodyParserConfig })
7. 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 ที่นี่ (ไม่มี timeout — ดู 3.2.7)
Apollo + Express wiring
import express from 'express'
import routes from './routes'
import typeDefs from './typeDefs'
import resolvers from './resolvers'
import schemaDirectives from './directives'
import { ApolloServer } from 'apollo-server-express'
import { API_GRAPHQL_PATH, APP_NO, APP_PORT, IS_PRODUCTION } from './config'ใช้ apollo-server-express (Apollo v2 — ปัจจุบัน EOL, modernize เป็น @apollo/server v4)
const server = new ApolloServer({
typeDefs,
resolvers,
schemaDirectives,
context: async ({ req, res }) => { /* ... */ },
formatError: (err) => { /* ... */ },
introspection: !IS_PRODUCTION,
playground: !IS_PRODUCTION,
debug: !IS_PRODUCTION,
plugins: [() => new GraphQLLogging()]
})Playground / introspection / debug
introspection: !IS_PRODUCTION,
playground: !IS_PRODUCTION,
debug: !IS_PRODUCTION,ทั้งสาม gate ด้วย IS_PRODUCTION (config.js:108 นิยาม IS_PRODUCTION = NODE_ENV === 'production') — ต่างจาก vtrc-api ที่ hardcode true ตรงนี้ถือว่า cu-central-api ทำถูกกว่า
มี 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 ก่อน) เป็น evidence ว่าไม่มี lint rule ที่จะ catch statement ที่ไม่มีผล
CORS
ไม่มี CORS middleware ใน codebase — grep ของ cors ใน cu-central-api/main-api/src/ ได้ 0 hits ที่เกี่ยวข้อง (มีแค่ corss ใน dependency list ของ email-templates ซึ่งไม่เกี่ยว) CORS (ถ้ามี) อยู่ที่ nginx proxy นอก Node
ต่างจาก vtrc-api ที่มี CORS bug (corss typo ใน config)
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:3 — selects which cron jobs run |
API_GRAPHQL_PATH | /api/graphql | config.js:8 |
Latent bug ใน Docker
FROM node:12.18.3
...
EXPOSE 9000Dockerfile expose 9000 แต่ runtime ฟัง APP_PORT (default 8080) ส่วน docker-compose.yml 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 บางฉบับบอกว่า 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 เท่านั้น
const db = new Sequelize(DB_NAME, DB_USERNAME, DB_PASSWORD, {
host: DB_HOST,
port: DB_PORT,
dialect: DB_SCHEMA,
dialectOptions: {
options: {
requestTimeout: 30000,
useUTC: false,
dateStrings: true,
typeCast: true,
timezone: TIME_ZONE
}
},
// ...
})driver คือ tedious (cu-central-api/main-api/package.json:42 — version ^9.2.1 ต่ำกว่า current 18+)
Pool sizing — risk เรื่อง connection limit
| Connection | pool.max | ที่มา |
|---|---|---|
db | 100 | connector.js:28 |
dbHr | 100 | connector.js:55 |
dbHrTemp | 50 | connector.js:83 |
dbMasDb | 100 | connector.js:110 |
dbHRMI_test | 100 | connector.js:137 |
dbHRMI_Center_HIS | 100 | connector.js:165 |
pool.max: 100 × 5 active connections × 3 replicas = สูงสุด 1,450 MSSQL sessions ในกรณีเลว (ไม่นับ dbHrTemp ที่ 50) — ทำให้ 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,
// ...
pool: {
max: 100,
min: 0,
acquire: 60000,
idle: 10000
}
})// 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 ใน connector.js และ loader comment ใน models/index.js
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 _stringify (date, options) {
date = this._applyTimezone(date, options)
// Z here means current timezone, _not_ UTC
// return date.format('YYYY-MM-DD HH:mm:ss.SSS Z')
return date.format('YYYY-MM-DD HH:mm:ss.SSS')
}force date format YYYY-MM-DD HH:mm:ss.SSS ให้ทุก DATE field ทุก connection — เป็น hack เพราะ MSSQL/Sequelize บางครั้ง format ไม่ตรงกัน ถือเป็น code smell ที่บ่งบอกว่าทีมไม่ได้คิดเรื่อง type handling ตอน design
syncModelToTable whitelist — auto-sync ใน prod
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
for (const i in models) {
const modelName = models[i]
if (skipModel.indexOf(modelName) === -1) {
await Models[modelName].sync({ force: false }).then(synced => {
console.log(`# -- Model#${+i + 1} "${modelName}" has been synced. -- #`)
return synced
}).catch(e => {
console.error('Sync model to table ERROR: ', e)
})
}
}ปัญหาเดียวกับ vtrc-api (CORR-CU-03)
db
.authenticate()
.then(async () => {
console.log('[MAIN] ✅ Connection has been established successfully (db).')
// Sync model to table
syncModelToTable()
})
.catch(err => {
console.error('[MAIN] ❌ Unable to connect to the database (db):', err)
})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 — commented out แล้ว
// Check default data is exists.
// if (typeof defaultData[modelName] !== 'undefined') {
// // Skip when data more than one record.
// Models[modelName].findAll({ limit: 1 }).then(rows => {
// if (rows.length === 0) {
// // Insert default data to table.
// Models[modelName].bulkCreate(defaultData[modelName]).then(added => {
// console.log(`Data from model "${modelName}" has been added ${defaultData[modelName].length} record(s).`)
// return added
// })
// }
// return null
// })
// }findAll + bulkCreate chain ถูก comment ทิ้งไปแล้ว — ในอดีตเคยเป็น fire-and-forget chain (ไม่ await) ที่เปิดช่อง race condition ถ้า boot หลาย replica พร้อมกัน
Cron jobs + APP_NO gating
import schedule from 'node-schedule'
import { APP_NO } from './config'
import * as cronMethods from './lib/cronMethods'
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" (cron.js:20) | แท้จริงแล้วคือ 40 3 * * * = 03:40 (ต่างกัน 5 นาที) |
console.log("In cron funciton.") (cron.js:26) | 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 19 Roadmap
สรุปการ 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-CU-03)
└── dbHr.authenticate() (fire-and-forget)Total boot time ปกติ ~3-5 วินาที — ขึ้นกับ AD + MSSQL responsiveness
เคล็ดลับตอนทำงานจริง
ถ้า boot ช้าหรือ hang
เช็ก checkAdAuthenConnection() ก่อน — LDAP/AD connectivity เป็นจุดที่ block boot ได้ ไม่มี timeout
ถ้าเจอ MSSQL connection timeout
เช็กจำนวน replica × pool.max ก่อน — ลด pool.max ใน connector.js หรือเพิ่ม MSSQL max_connections
dbHRMI_test — ถ้าจะ cleanup
ลบทั้ง connection ใน connector.js และ comment ที่เหลือใน models/index.js ออกพร้อมกัน อย่าลบแค่ด้านเดียว ไม่งั้น export ใน connector.js:195 จะ reference ตัวแปรที่ไม่มี
อ่านต่อ → 3.2.3 Authentication + LDAP/AD flow