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 併發實驗採納;適用限制見下文。 此值只限制採用共用名額的同步產生流程,不是使用者上限,也不替代背景執行緒池設定。 名額由單一reportGenerationSlotsSemaphore 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 均須逐一完成以下步驟。
- 先交付字型,再部署新版。 確認各環境 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。 - 規格、實作與前端同批承接。 發布 immutable reference 後,在 nbsos 採用 report、移除重複字型/名額組裝,
改用共用 eager renderer 及 qualified 名額。修訂
specimen-reports.md的字型行為、設定鍵、 部署步驟與同步預設上限;移除SpecimenReportService對 renderer 初始化失敗的rpt001.fonts-unavailable/rpt002.fonts-unavailable503 分支及舊延遲重試說明。503的忙碌錯誤仍保留,不因移除字型錯誤而移除其他合法錯誤處理。 同時核對nbsos-office/src/nls/term/zh-TW.ts與initial-exam-review-page.test.tsx,移除無其他使用者的字型未就緒翻譯與舊契約測試案例。 - 改驗證啟動契約,再驗證交付。 將
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 筆單份 p95 | 10,000 筆單份 p95 | 10,000 筆 heap 峰值 | 10,000 筆 RSS 峰值 | 一般 API p95(大報表期間) |
|---|---|---|---|---|---|---|
| 2 | 1.406 秒 | 351 ms | 5.987 秒 | 822 MiB | 1,495 MiB | 50 ms |
| 4 | 0.872 秒 | 449 ms | 6.452 秒 | 1,333 MiB | 2,033 MiB | 52 ms |
| 8 | 0.655 秒 | 587 ms | 8.217 秒 | 1,785 MiB | 2,522 MiB | 53 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 承接仍需自己的驗證。