Skip to content

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

1:32:main-api/src/index.js
const server = new ApolloServer({
  typeDefs,
  resolvers,
  schemaDirectives,
  context: async ({ req, res }) => {

ใช้ apollo-server-express (Apollo v2 — ปัจจุบัน EOL, modernize เป็น @apollo/server v4)

Playground / introspection / debug

77:79:main-api/src/index.js
  introspection: !IS_PRODUCTION,
  playground: !IS_PRODUCTION,
  debug: !IS_PRODUCTION,

ทั้งสาม gate ด้วย IS_PRODUCTION — ต่างจาก vtrc-api ที่ hardcode true (บท 9.10) ตรงนี้ถือว่าทำถูกกว่า

มี defense-in-depth อีกชั้นใน resolver

1:14:main-api/src/resolvers/main.js
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

88:90:main-api/src/index.js
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}`)
ConfigDefaultSource
APP_PORT8080config.js:20
APP_NO'1'config.js — selects which cron jobs run
API_GRAPHQL_PATH/api/graphqlconfig.js:8

Latent bug ใน Docker

18:18:main-api/Dockerfile
EXPOSE 9000

Dockerfile 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

#VariableSchema envใช้ทำอะไรExportAuth ตอน boot
1dbDB_NAMEcore tables (account, otp, verifycode, ...)
2dbHrDB_HR_NAMEHRMI employee + organization (ใหญ่สุด)
3dbHrTempDB_HR_DATA_LAKE_NAMEstaging tables (taxes, address, phone)
4dbMasDbDB_MASTER_DATA_NAMEmaster data
5dbHRMI_testhardcoded "dbHRMI_test"dead (ไม่มี model ผูก)
6dbHRMI_Center_HISDB_HRMI_CENTER_HISheal-card discount + log

ทั้ง 6 ใช้ DB_USERNAME/DB_PASSWORD/DB_HOST/DB_PORT เดียวกัน — เปลี่ยน database name เท่านั้น

13:21:main-api/src/connector.js
  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

Connectionpool.max
db100
dbHr100
dbHrTemp50
dbMasDb100
dbHRMI_test100
dbHRMI_Center_HIS100

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

117:142:main-api/src/connector.js
const dbHRMI_test = new Sequelize("dbHRMI_test", DB_USERNAME, DB_PASSWORD, {
  host: DB_HOST,
  port: DB_PORT,
  dialect: DB_SCHEMA,
  // ...
})
95:105:main-api/src/models/index.js
// 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

40:93:main-api/src/models/index.js
// [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

SubdirConnectionSchema
models/core/dbCENTRALIZE_APP
models/hrmi/dbHrdbHRMI_Center
models/hrRawData/dbHrTempCENTRALIZE_HR
models/masterData/dbMasDbCENTRALIZE_MASTER_DATA
models/hrmiHis/dbHRMI_Center_HISdbHRMI_Center_HIS
models/hrmi_test/dbHRMI_test(dead — commented out)

Global DATE monkey-patch

11:17:main-api/src/models/index.js
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

4:6:main-api/src/lib/model.js
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)

99:104:main-api/src/index.js
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

javascript
if (!IS_PRODUCTION) {
  await syncModelToTable()
}

Default data seeding — bug เดียวกับ vtrc-api

javascript
// ใน 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 + bulkCreate chain เป็น fire-and-forget (ไม่ await) — race condition ถ้า boot หลาย replica พร้อมกัน
  • ทำให้ seeding อาจทำงานซ้อนกันใน deployment 3 replicas

Cron jobs + APP_NO gating

1:32:main-api/src/cron.js
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 jobs

3 jobs ที่ run เฉพาะ replica ที่ APP_NO === 1

ScheduleMethodหน้าที่
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