跳至主要内容

ADR-035: 報表共用字型、同步容量與背景作業分離

狀態: 已接受 (Accepted) 審閱: 2026-10-05 第 3 輪通過;R1-01~03、R2-01 均已處置,無未處置意見 日期: 2026-10-04 適用範圍: app-tenant-server/app-tenantless-server,自 app-v2026.1.23 起(2026-10-05 發布) 局部修訂: ADR-029 的 report feature 組裝;字型資產沿用 ADR-033 的啟動期驗證政策。

背景​

僅同步報表也需要字型與 PDF renderer。原 report feature 同時帶入 auth、file、scheduling、 背景執行緒池與附屬 report job 資源;只為重用字型組裝而安裝完整背景流程,超出了同步報表的需求。 字型目錄已有可預測的應用根目錄預設,不應成為每個環境都必須另填的參數。

決策​

  • report 提供 ReportFonts、ReportRenderer、FixedPdfRenderer 與同步名額的組裝;沒有其他 feature 依賴。
  • report-job 另行選用,依賴 report、auth、file、scheduling,承接背景執行緒池、維護排程及附屬 report 資源。 Java package 使用 feature/reportjob;既有 job 的業務層 package 仍為 service/report 等。
  • app.fonts 是共用字型目錄,預設 ${APP_FONTS:${app.home}/var/fonts}。 app.report.fonts 是報表目錄,預設 ${APP_REPORT_FONTS:${app.fonts}};兩者都是目錄字串。 字型選擇另由 app.report.default-font-family 與 app.report.font-families 承接。
  • app.report.max-concurrent-generations 的預設是 4,依本批 2/4/8 併發實驗採納;適用限制見下文。 此值只限制採用共用名額的同步產生流程,不是使用者上限,也不替代背景執行緒池設定。 名額由單一 reportGenerationSlots Semaphore bean 提供;所有同步服務使用 @Qualifier("reportGenerationSlots") 明確注入,避免其他 Semaphore bean 造成歧義或接錯。 於取數前 tryAcquire(),額滿立即回忙碌,不排隊或轉非同步,並在成功、失敗及取消的 finally 釋放。 同步名額與 app.report.executor.threads 是獨立上限;同時安裝兩個 feature 時, 預設最多 4 份同步加 2 份背景報表同時渲染,合計 6 份。背景排隊的 job 不占渲染名額。
  • 字型是外部部署資產,不由應用下載、產生、寫回,也不進 WAR。採用 report 的應用在啟動時 建立並驗證字型與兩種 renderer;缺少目錄/檔案、檔案不完整或損壞即阻止 server 啟動。 此要求同時適用於僅同步及 report-job;不提供缺字型時先啟動的模式。 補齊或修復檔案後重新啟動;替換字型同樣須重啟,不承諾熱更新。
  • 零或負值容量在建立共用名額 bean 時即啟動失敗。字型及容量驗證不因呼叫端採用 ObjectProvider 而延後;字型 bean 明訂非延遲初始化,沿用 ADR-033 的啟動期驗證原則。
  • 本批不保留舊 app.report.fonts.dir/APP_REPORT_FONTS_DIR 相容;使用者確認目前僅開發環境。 不改寫既有發布版 Feature Changelog、PDF 接受帳本或資料庫 migration。

取捨​

單一 report feature 加 jobs.enabled 開關會令來源 partition 與 runtime 開關形成兩套選擇, 並讓同步 scaffold 仍帶入 job entity,因此採獨立 feature。只在 nbsos 保留字型組裝則會複製共用機制。

啟動期驗證讓缺字型的部署立即失敗,避免開始提供服務後才發現報表不可用;安裝字型的責任屬部署。 字型目錄提供預設值不代表可省略資產交付。 4 是依有限環境驗證採納的預設,不是所有硬體及所有 RPT 的吞吐保證;部署仍須按實際容量覆寫與驗證。

承接與驗證​

兩個 source 各自更新 FEATURE_CHANGELOG 與 catalog。僅同步的下游只採用 report; 已使用背景作業的下游同時採用 report-job,協調舊 core 移除、新 core 安裝及既有 reference imports。 骨架 app-server.yml 的共用目錄與 optional import 必須另行承接。

本批已通過 peer review;提交及切參考實作 release 後,nbsos 透過不可變 release 採用, 不把未提交 source 冒充已發布 Feature 來源。其兩份 RPT/HTML/PDF 仍沿原規格與接受帳本, 承接後重新核對來源及 PDF bytes,未涵蓋真實資料與實體列印驗收。

驗證包含無 job/DB 的 context、預設目錄及覆寫、缺檔/損壞阻止啟動、修復後重啟、 report-job 同樣的啟動驗證、其他 Semaphore 共存時明確注入、容量邊界、背景 job 回歸、 feature partition 與 2/4/8 併發下的報表耗時、heap/RSS 和一般 API 延遲。

nbsos 的契約遷移與部署順序​

nbsos 現行 nbsos-docs/docs/design/api-specs/specimen-reports.md 及 SpecimenReportFontsIT 允許缺字型時啟動,報表請求回 503/rpt001.fonts-unavailable 或 rpt002.fonts-unavailable; 補齊檔案可在同一 context 重試。這是承接前的契約,採用本 release 後改為啟動失敗。 此變更影響整個 server 是否可用,不只是兩張卡片的錯誤提示。現有遠端字型尚未安裝的紀錄 不構成新版的部署前置條件已完成;SIT/UAT 均須逐一完成以下步驟。

  1. 先交付字型,再部署新版。 確認各環境 Tomcat 的實際 app.home,將與已接受樣張相同、 按固定 SHA-256 校驗的 NotoSans-Regular.ttf、NotoSans-Bold.ttf、NotoSansTC-Regular.ttf、 NotoSansTC-Bold.ttf 及 LICENSE-NotoSans.txt、OFL-NotoSansTC.txt 放到 ${app.home}/var/fonts。 使用自訂共用目錄時,以持久啟動設定 APP_FONTS/app.fonts 指定;必要的報表專用覆寫則為 APP_REPORT_FONTS/app.report.fonts。確認 Tomcat 執行帳號能讀目錄與檔案,複製後再次核對指紋, 完成後才能部署採用新版 report 的 WAR。不能沿用開發機快取路徑或舊 APP_REPORT_FONTS_DIR。
  2. 規格、實作與前端同批承接。 發布 immutable reference 後,在 nbsos 採用 report、移除重複字型/名額組裝, 改用共用 eager renderer 及 qualified 名額。修訂 specimen-reports.md 的字型行為、設定鍵、 部署步驟與同步預設上限;移除 SpecimenReportService 對 renderer 初始化失敗的 rpt001.fonts-unavailable/rpt002.fonts-unavailable 503 分支及舊延遲重試說明。 503 的忙碌錯誤仍保留,不因移除字型錯誤而移除其他合法錯誤處理。 同時核對 nbsos-office/src/nls/term/zh-TW.ts 與 initial-exam-review-page.test.tsx,移除無其他使用者的字型未就緒翻譯與舊契約測試案例。
  3. 改驗證啟動契約,再驗證交付。 將 SpecimenReportFontsIT 的「缺字型仍能啟動、HTTP 503、 同一 context 補齊後重試」改為啟動驗證,涵蓋缺目錄、缺檔、損壞字型均阻止啟動, 修復後重新啟動成功;資產齊備時重跑 defaults、兩份同步 PDF、授權與忙碌錯誤回歸。 部署後檢查 server 啟動/健康及有權限使用者的兩份真實 PDF,不能只靠健康 UP 代替報表驗證。 九份合成 PDF 與 HTML 接受證據依 report 流程重新核對來源、字型及完整 PDF bytes 後承接, 不改寫舊帳本或把合成結果當成真實資料/紙本列印驗收。

本批只記錄承接契約,未修改 nbsos 程式/規格,也未安裝遠端資產或部署。 承接前仍由其現行版本提供原有行為;承接及上述環境驗證屬既有交付順序,不另開 follow-up。

併發實驗(2026-10-05)​

使用 nbsos 已接受的兩份同步報表,在本機 20 logical processors、Java 25.0.1、 2 GiB heap/2 GiB direct-memory 上限下比較 2、4、8;沒有並行建置工作。 每個配置啟動獨立測試 JVM,字型與各報表預熱後執行三輪,第二輪順序反轉。 MockMvc 呼叫真實授權、service、H2 查詢與 Java PDF,資料完全合成,PDF 僅留在記憶體。 一般 API 探針為 GET /api/v1/initial-screening-records?size=20,與報表在同一 JVM/DB pool。

下表是三輪中位數;heap/RSS 是各輪觀測峰值的中位數,採樣間隔約 50 ms。 500 筆情境每輪都有 16 次呼叫,兩份報表各半;測試客戶端以對應數量的 worker 驅動, 客戶端尚未送出的工作不代表伺服器排隊。10,000 筆情境同時呼叫 N 份初檢報表, N 等於所測上限,因此只比較單份耗時與資源壓力,不把不同工作量的整批耗時直接比較。

併發500 筆整批耗時500 筆單份 p9510,000 筆單份 p9510,000 筆 heap 峰值10,000 筆 RSS 峰值一般 API p95(大報表期間)
21.406 秒351 ms5.987 秒822 MiB1,495 MiB50 ms
40.872 秒449 ms6.452 秒1,333 MiB2,033 MiB52 ms
80.655 秒587 ms8.217 秒1,785 MiB2,522 MiB53 ms

三輪全部報表回 200,一般 API 探針無錯誤。4 提高同批吞吐,仍保留較多記憶體餘裕; 8 的吞吐仍較高,但單份耗時與資源成本上升。因此選 4 作有限的預設起點。 RSS 包含 heap、native memory 等,不能把 2 GiB heap 上限等同於 2 GiB 容器需求。 本實驗只測同步報表,沒有證明同步與背景混合工作的容量。兩個 feature 都安裝時, 記憶體預算須合計同步名額與背景 executor threads(預設 4 + 2 = 6)。 若同步取數/渲染亦持有 DB 連線,報表相關連線需按同步名額 + executor threads + 1 條背景心跳估算, 再加其他 API/排程等餘裕;只看 executor threads + 1 的既有警告不足以涵蓋混合工作。 同步流程若在渲染前釋放交易,DB 占用須按該實作實測;記憶體仍須考慮同時渲染的總數。

這是 H2/合成短文字的探索性實驗,不是 SIT 真實 DB、網路、長文、所有 RPT 或部署 SLA 的容量認證。 framework 的新組裝另由 ReportConfigTest 驗證;本實驗用的是 nbsos 原有相同 Semaphore admission 行為, 不代表 nbsos 已採用新版 feature。原始三輪摘要與可重跑的程式留在本次 peer-review evidence, 本 ADR 保存結果與限制;來源釋出後的 nbsos 承接仍需自己的驗證。