Bỏ qua để đến nội dung

Scale & vận hành

Quyết định nền: trust model v1 giữ module-level

Phần tiêu đề “Quyết định nền: trust model v1 giữ module-level”

Mọi agent có helpdesk:read thấy mọi conversation, không phân biệt inbox/team/assignee — chủ ý cho team nhỏ, không phải thiếu sót. Đổi lại: mọi list/counts endpoint từ Wave 2 (B1) trở đi bắt buộc nhận filter param (inbox_id, assignee_id, …) dù server chưa dùng chúng như authorization boundary — để khi cần row-scoping thật, chỉ thêm 1 lớp WHERE server-side, không phải đổi API shape/client contract.

A1 · Backend perf

SLA cron batch label qua 1 query whereIn (thay vì pluck per-conversation) + select hẹp cột + cảnh báo tick >30s. automationInFlight guard chuyển vào services/automation/engine.ts để sla.ts dùng chung runGuarded.

A2 · FE cleanup

Composer surface lỗi gửi (toast), xoá dead code, :popper v3→v4 (8 chỗ), bỏ mock folder khỏi production path, xoá 5 plugin no-op.

B1 · Pagination + counts

List conversations server-side filter/sort/page; endpoint GET /conversations/counts (Knex aggregate 1 lần); FE fetchPage + ensureOne (fix deep-link) + ticker merge-not-replace.

B2 · Reports aggregation

GET /helpdesk/reports/overview Knex aggregate (COUNT, AVG response/ resolution, CSAT) — không load message content; xoá fan-out ensureLoadedForMany phía FE.

Endpoint Default Clamp max Ghi chú
List CRUD chung (crud.factory.ts) 100 500 Kể cả limit=-1 (sentinel “không giới hạn” kiểu cũ) cũng bị clamp về 500
/messages (flat) 50 200
/conversations/:id/messages (thread lồng) không giới hạn mặc định 1000 Thread dài hiếm khi vượt — chủ ý không cắt mặc định như list
conversations list (B1) 25 (PAGE_SIZE FE) 100 (server cap) refetchCurrent gộp page 1..currentPage trong 1 request, cap ở 100
IMAP MAX_PER_TICK 50 message/connection/tick Cron * * * * * → throughput cap thực tế 50 msg/phút/connection
Messages store client (eviction) 2000 message / 20 conversation LRU theo conversation, không theo từng message lẻ

Vượt bất kỳ cap nào ở trên không mất dữ liệu — chỉ đợi request/tick kế tiếp (list phân trang tiếp, IMAP watermark chỉ tiến tới message cuối xử lý xong).

Ba cơ chế in-memory per-process — rủi ro multi-instance

Phần tiêu đề “Ba cơ chế in-memory per-process — rủi ro multi-instance”

Cả 3 đều là state sống trong RAM của một process Node, không chia sẻ qua Redis/DB. Chạy 1 instance thì an toàn tuyệt đối; chạy ≥2 instance thì có khoảng hở nhất định.

app-access.ts
const cache = new Map<string, CachedPermissions>();
const CACHE_TTL_MS = 60_000;

Key = policy id đã sort (không phải theo user). Cache không invalidate theo event — chỉ hết hạn theo TTL. Thu hồi quyền trên 1 instance vẫn có thể được instance khác “chấp nhận” tới 60s sau nếu request rơi vào đúng instance đó.

runGuarded (chuyển vào services/automation/engine.ts từ đợt A1) chỉ chặn re-entrant automation trong cùng process — 2 instance nhận cùng lúc 1 event status_changed từ 2 request khác nhau vẫn có thể chạy automation song song ở 2 process riêng biệt.

imap-source.ts:19-24
// In-process guard so a slow tick (mailbox chậm) doesn't overlap the next tick
// within the SAME instance. Cross-instance is handled by context.schedule.
let pollInFlight = false;

Đây là boolean module-level, chỉ ngăn tick chồng tick trong 1 instance. An toàn cross-instance (không 2 instance cùng poll 1 connection) được uỷ quyền hoàn toàn cho context.schedule của ODP core (leader-election kiểu SynchronizedClock) — nếu deploy custom scheduler không tương thích cơ chế này, có nguy cơ 2 instance cùng poll 1 mailbox.

Giới hạn còn lại (không phải multi-instance)

Phần tiêu đề “Giới hạn còn lại (không phải multi-instance)”
  • RAM buffer IMAP: tối đa 50 message thô/connection/tick giữ trong bộ nhớ tiến trình trong lúc xử lý — nhiều connection poll song song với mailbox lớn (đính kèm nặng) có thể cộng dồn đáng kể dù chỉ 1 instance.
  • isEncrypted() là heuristic prefix-string (crypto.ts), không phải structural check — đủ dùng cho secret nội bộ nhưng không phải bằng chứng mã hoá tuyệt đối; đừng tái dùng để validate input từ nguồn không tin cậy.
  • i18n chưa xongi18n/locales/en.json/vi.json thiếu nhiều key, phần lớn UI vẫn hardcode tiếng Anh.
  • Per-button permission gating chưa xong — nav-level gating đã có (navigations.ts), nhưng UI chưa tự ẩn/disable từng nút theo capability; server vẫn chặn đúng qua 403, chỉ là UX chưa mượt (user bấm rồi mới thấy lỗi).