Skip to content

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

Latent bug ใน Docker

FROM node:12.18.3
...
EXPOSE 9000

Dockerfile 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

#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 เท่านั้น

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

Connectionpool.maxที่มา
db100connector.js:28
dbHr100connector.js:55
dbHrTemp50connector.js:83
dbMasDb100connector.js:110
dbHRMI_test100connector.js:137
dbHRMI_Center_HIS100connector.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

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

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

javascript
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 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" (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