CareMaker · Báo cáo điều tra kỹ thuật

Lỗi: Sau Quick Edit, shift biến mất khỏi màn hình
(クイック編集後にシフトが画面から消える事象)

Phân tích toàn diện luồng xử lý từ lúc bắt đầu chỉnh sửa đến khi hiển thị kết quả, đối chiếu mã nguồn Frontend (Next.js), Backend (Kotlin/R2DBC) và hạ tầng AWS. Kết luận: lỗi có 2 tầng nguyên nhân, trong đó tầng kích hoạt mang tính đặc thù môi trường production.

Phạm vi
Màn スケジュール管理 / Quick Edit
Môi trường
Safari / iPad (Prod)
Tần suất
Cực hiếm · 1 user 2×
Mất dữ liệu?
Không — chỉ hiển thị

1 Tóm tắt điều hành

Kết luận nhanh dành cho người đọc không kỹ thuật. Chi tiết triệu chứng xem mục 2 · Hiện tượng.

Xếp hạng nguyên nhân (đã cập nhật theo thông tin hiện trường)

#Giả thuyếtTầngMức độ phù hợp
A Lỗi compositing/repaint của WebKit trên timetable ảo hóa. React cập nhật DOM (shift đã có) nhưng Safari iPad không repaint lớp transform/sticky đến khi scroll/reflow → shift "vô hình". Reload/đổi ngày = re-composite → hiện. Client · WebKit ★★★ Cao nhất
Khớp cả 4 dữ kiện: Safari iPad · hiếm · reload/đổi ngày khỏi · data đã lưu
B Refetch sau khi lưu bị bỏ lỡ, không có đường khôi phục. iOS treo/hủy fetch đang bay khi trang chớp mất foreground; refetchOnWindowFocus:false + skipInvalidate:true → cache không cập nhật → kẹt đến khi fetch lại. Client · React Query ★★ Cao
Đặc thù iOS, hiếm, reload/đổi ngày (queryKey mới) khỏi
C Race state lạc quan ở frontend (skipInvalidate + ghi đè dataSourceFilter, queryKey ngày không đổi). Làm UI dễ vỡ trước A/B. Client · State ★ Yếu tố khuếch đại
D Aurora replica-lag (đọc reader sau khi ghi writer). Giả thuyết ban đầu — nay hạ cấp. Backend · Hạ tầng ↓ Đã hạ cấp
Lag đo thực chỉ ~15ms (đỉnh 49ms) → không thể gây "kẹt"; không giải thích được tính Safari-iPad-specific và độ hiếm (mục 📊)
🔬
Chưa thể chốt A hay B chỉ bằng đọc code — nhưng không cần chốt vẫn sửa được: giải pháp S3 (mục 8) phủ cả hai. Việc chẩn đoán (soi DOM trên iPad — mục 7) chỉ là tùy cơ vì lỗi không ép tái hiện được; quy tắc phân biệt nếu may bắt được: node shift CÓ nhưng không vẽ ⇒ A (paint); node shift KHÔNG có (cache cũ) ⇒ B (refetch lỡ).
Client
Tầng nghi vấn chính (WebKit/iPad)
Safari iPad
Môi trường tái hiện
0
Bản ghi bị mất trong DB
1 user · 2×
Tần suất (cực hiếm)

2 Hiện tượng (triệu chứng & hiện trường)

Mô tả chính xác những gì khách báo và bối cảnh quan sát được — cơ sở cho mọi phân tích phía sau.

⚠️
Triệu chứng (khách báo): Khi chỉnh sửa shift bằng Quick Edit rồi bấm「今回のみ」hoặc「今後すべて」, shift vừa sửa biến mất khỏi màn hình (dù đã lưu thành công). Có 2 cách để hiển thị lại: (1) reload trang, hoặc (2) chuyển sang ngày khác rồi quay lại. Dữ liệu trong DB vẫn đúng (không mất).
🆕
Thông tin hiện trường bổ sung (rất quan trọng cho chẩn đoán):
  • Cực hiếm — chỉ ghi nhận ở 1 user, 2 lần.
  • Xảy ra trên Safari / iPad (WebKit, thiết bị cảm ứng).
  • Khôi phục bằng reload hoặc đổi ngày (cả hai đều buộc fetch + render lại).
→ Ba dấu hiệu này không khớp với một lỗi replica-lag mang tính hệ thống (vốn ảnh hưởng mọi user/mọi trình duyệt và thường xuyên). Chúng là chữ ký của một lỗi phía client đặc thù iOS Safari (iPad) — xem xếp hạng nguyên nhân ở mục 1 và phân tích ở mục 6.
🧩
Vì sao reload HOẶC đổi ngày đều khỏi: cả hai đều buộc một lần fetch + render mới — reload remount toàn bộ trang; đổi ngày làm đổi queryKey → truy vấn mới + dựng lại toàn bộ cột timetable (kéo theo repaint). Việc chỉ cần fetch+render mới là khỏi (không cần sửa DB) khẳng định lỗi nằm ở render/đồng bộ state phía client, không phải mất dữ liệu.

Kiến trúc hạ tầng AWS liên quan · bối cảnh cho mục 3

Trích xuất từ caremaker-infra/terraform-backend và cấu hình app caremaker-alohomora/lib/mysql-base. Trọng tâm: tách writer/reader của Aurora.

Frontend — Next.js (caremaker-horizon)
React 19 · React Query v3 · chạy trên trình duyệt người dùng
CloudFront → ALB + WAF
HTTPS · định tuyến tới ECS
ECS Fargate — Backend "alohomora" (Kotlin, R2DBC reactive)
Production: nhiều task sau ALB · DB host lấy từ Secrets Manager
Aurora MySQL — WRITER
primary · alohomora-release-rds.cluster-…
⬅ GHI (bulk upsert)
Aurora MySQL — READER REPLICA
secondary · …cluster-ro-… · readOnly
⬅ ĐỌC (search timetable)
↳ Giữa Writer và Reader có độ trễ sao chép (replication lag) — đo thực ~15ms trung bình, đỉnh 49ms (xem mục 📊 Số liệu thực đo).
Đường GHI → Writer (mạnh, commit ngay) Đường ĐỌC → Reader replica (có thể trễ) Tầng ứng dụng (ECS)

Các tầng đã loại trừ (không phải nguyên nhân)

TầngKết luận
Redis / ElastiCacheChỉ cache auth/session, không cache kết quả search shift → loại trừ.
SQS / SNS / Step FunctionPhục vụ job "statics" định kỳ, không liên quan tạo/hiển thị shift → loại trừ.
ALB / WAF / CloudFrontSearch là POST → không bị cache phía CDN → loại trừ.
Xử lý ghi bất đồng bộBulk upsert ghi đồng bộ trong 1 transaction, commit xong mới trả về → loại trừ.

3 Workflow xử lý hiện tại — từ lúc edit đến khi hiển thị

Sơ đồ tuần tự (UML sequence). Mỗi cột là một tác nhân với đường đời (lifeline) dọc; mũi tên là thông điệp trao đổi theo thời gian (trên → dưới). Nét liền = lệnh gọi, nét đứt = phản hồi. Vùng đỏ là nơi phát sinh lỗi.

ℹ️
Sơ đồ dưới minh họa giả thuyết D (replica-lag) — hữu ích để hiểu đường ghi/đọc tách writer–reader. Với ca Safari/iPad mà khách báo, điểm hỏng nhiều khả năng dịch về phía client (render/refetch) tại bước 9→12 — xem mục 6 · Phương pháp tái hiện & chẩn đoán và bảng xếp hạng nguyên nhân ở mục 1.

3.1 — Sơ đồ tuần tự (Sequence Diagram)

① CHỈNH SỬA (cục bộ) ② LƯU → WRITER (đúng) ③ ĐỌC LẠI → READER REPLICA ⚠ VÙNG LỖI ④ KHÔI PHỤC THỦ CÔNG (đổi ngày) Người dùngTrình duyệt FrontendReact · React Query Backend APIECS · alohomora Aurora WRITERprimary Aurora READERsecondary · cluster-ro Vào chế độ Quick Edit (activeQuickEdit=true) 1 Kéo–thả shift (chỉ sửa state cục bộ) 2 dataSourceFilter cập nhật lạc quan (shift mang id tạm: _RAW / CLONED) Bấm「今回のみ」/「今後すべて」 3 POST bulk upsert (mutateAsync) 4 Ghi trong transaction nguyên tử 5 Commit OK ✓ — DB đã đúng 6 200 · ShiftUpdateResponse (isSucceed=true) 7 onSuccess → invalidate → refetch search 8 searchWithUnassigned() → ShiftRosterSecondaryDao (reader) 9 Dữ liệu CŨ ✗ — replica chưa đồng bộ (lag) 10 Trả data cũ (thiếu shift) 11 12 Effect ghi đè dataSourceFilter bằng data cũ → SHIFT BIẾN MẤT khỏi màn hình 13 queryKey (ngày) không đổi → không refetch lại → màn hình KẸT cho tới khi đổi ngày Đổi sang ngày khác rồi quay lại 14 Refetch (queryKey MỚI) — replica đã bắt kịp 15 Data ĐÚNG ✓ → shift hiển thị trở lại 16
Lệnh gọi (nét liền) Phản hồi (nét đứt) Vùng lỗi (đọc reader bị lag) Bước đúng / khôi phục

3.2 — Diễn giải chi tiết từng bước

  1. Vào Quick Edit
    Cờ activeQuickEdit bật. Dữ liệu shift của ngày đang xem được tải qua useShiftTimetable.
  2. Tải & đồng bộ dữ liệu nền
    dataSource (từ server) → filteredDataSource → effect tại index.tsx:920 sao chép vào state cục bộ dataSourceFilter (có chốt isSame bằng deepEqual).
  3. Kéo–thả shift
    Handler handleDropResultShiftItem / onDropStaffColumn chỉ chỉnh state cục bộ; shift mang id tạm (_RAW, CLONED, ACCOMPANY). Màn hình render theo state lạc quan này.
  4. Bấm「今回のみ」/「今後すべて」
    onSubmitQuickEdit(recurrence) tại tools.tsx:1166: gom editedShifts, kiểm tra trùng lịch, dựng payload.
  5. Gửi ghi & backend commit (ĐÚNG)
    POST /crm/shift-calendar/bulk/...bulkUpsertShifts ghi đồng bộ vào WRITER trong transaction, trả response rỗng. DB đã đúng từ đây.
  6. Refetch trúng READER chưa đồng bộ (ĐIỂM LỖI)
    onSuccess gọi invalidateShiftCalendarQueries → refetch useShiftTimetable. Truy vấn đọc đi qua ShiftRosterSecondaryDao.searchWithUnassigned = reader replica. Do replication lag, replica trả về trạng thái cũ thiếu shift vừa sửa.
  7. State cục bộ bị ghi đè bằng dữ liệu cũ
    setActiveQuickEdit(false) + dữ liệu refetch (cũ) làm filteredDataSource tính lại; effect index.tsx:920 ghi đè dataSourceFiltershift biến mất khỏi UI. Vì skipInvalidate:true không có lần invalidate dự phòng, và queryKey (ngày) không đổi nên không có cơ chế tự đồng bộ lại.
  8. Khôi phục thủ công
    Đổi sang ngày khác → đổi queryKey → truy vấn mới; vài giây trôi qua đủ để replica bắt kịp → quay lại ngày cũ thấy shift hiển thị đúng.

3.3 — Vai trò của invalidateQueries (cơ chế đồng bộ duy nhất sau lưu)

invalidateQueries không phải chi tiết phụ — nó chính là cây cầu nối bước 8 → 9 → 12 trong sơ đồ trên. Hiểu đúng nó là chìa khóa để hiểu vì sao màn hình "kẹt".

Nó làm gì (React Query v3)

Đánh dấu các query khớp queryKey là stale; với query đang mounted (mặc định refetchActive:true) → refetch ngay lập tức. Đây chính là bước 8 (refetch sau lưu) trong sơ đồ tuần tự.

invalidateShiftCalendarQueries — hàm bao gom nhiều queryKey

Toàn bộ các mutation về shift không gọi invalidateQueries trực tiếp mà đi qua hàm tiện ích này (hook/useShift.ts:95) — nơi gom 5 nhóm queryKey liên quan tới lịch và invalidate song song bằng Promise.all. Đây là điểm tập trung hóa: được tái sử dụng ở ~11 mutation (tạo/sửa/xóa/bulk-upsert/optimize… — các dòng 565, 690, 748, 788, 829, 859, 893, 937, 961, 1096, 1131), nên hành vi của nó chi phối cách mọi thao tác ghi shift đồng bộ lại UI.

// hook/useShift.ts:95 — hàm bao được dùng chung cho ~11 mutation shift const invalidateShiftCalendarQueries = async (queryClient, date?) => { try { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["useShiftTimetable"] }), // timetable chính (đang hiển thị) ...(date ? [queryClient.invalidateQueries({ queryKey: ["useSearchShiftTimetable", date] })] : []), queryClient.invalidateQueries({ queryKey: ["useSearchShiftTimetable"] }), queryClient.invalidateQueries({ queryKey: ["useNgDesireTimeOffice"] }), queryClient.invalidateQueries({ queryKey: ["useShiftDetail"] }), ]); } catch (e) { console.warn(...) // NUỐT lỗi: refetch hỏng KHÔNG làm mutation reject } };

Lưu ý: ["useShiftTimetable"] được invalidate theo prefix (không kèm officeId/filter) → khớp mọi biến thể của query timetable đang active, gồm đúng query ngày đang xem. Còn useSearchShiftTimetable được invalidate cả date lẫn không date để phủ cache theo-ngày và cache tổng.

Hai điểm gọi trong luồng Quick Edit — chỉ một chạy

(a) ĐIỂM CHẠY — trong onSuccess của mutation

hook/useShift.ts:748 → gọi invalidateShiftCalendarQueries(queryClient, date) = đúng hàm bao mô tả ở trên (invalidate 5 nhóm queryKey song song). → Đây là refetch chính sau lưu (bước 8).

(b) ĐIỂM BỊ BỎ QUA — trong onQuickEditChange

index.tsx:749-754: khi thoát quick-edit, nếu !skipInvalidate sẽ invalidate lần hai (useShiftTimetable + useNgDesireTimeOffice). Nhưng submit gọi onQuickEditChange(false,{skipInvalidate:true}) (index.tsx:1105) → khối này bị skip = mất "nhịp invalidate dự phòng".

// index.tsx:749 — nhánh invalidate dự phòng KHÔNG chạy ở luồng submit (vì skipInvalidate:true) if (!value && !options?.skipInvalidate) { queryClient.invalidateQueries(["useShiftTimetable"]); queryClient.invalidateQueries({ queryKey: ["useNgDesireTimeOffice"] }); } // useShift.ts:251 — queryKey CHỨA ngày (trong filter) → invalidate refetch ĐÚNG key hiện tại, KHÔNG đổi key queryKey: ["useShiftTimetable", officeId, filter /* gồm ngày */, ...]

Vì sao đây là mắt xích then chốt của bug

  • One-shot, cùng queryKey: invalidate refetch đúng query ngày hiện tại một lần — không retry, không kiểm tra kết quả đã phản ánh thay đổi chưa. Đổi ngày làm đổi filter ⇒ queryKey mới ⇒ query mới (đường khôi phục); còn invalidate cùng-key thì không tự sửa được trạng thái đã hỏng.
  • Lỗi refetch bị nuốt: invalidateShiftCalendarQueries bọc try/catch (useShift.ts:114-120) để lỗi refetch không làm mutation reject. Hệ quả: nếu refetch do invalidate kích hoạt bị hủy/treo trên iOS, mutation vẫn báo success nhưng cache không được cập nhật (chỉ bị đánh dấu stale).
  • Không có đường tự khôi phục: kết hợp với refetchOnWindowFocus:false (ManagementLayoutWrapper.tsx:21) → quay lại foreground không tự refetch → kẹt tới khi reload/đổi ngày. Đây chính là cơ chế giả thuyết B.
🔑
Vấn đề không phải "invalidateQueries có được gọi hay không" — nó được gọi (điểm a). Vấn đề là nó chạy một lần, trên cùng queryKey, không tự xác thực kết quả, và lỗi refetch bị nuốt; nên khi lần refetch đó lỡ (B) hoặc kết quả không repaint (A) thì không còn nhịp invalidate nào cứu (đã skip ở điểm b). Các fix S2/S3/P1 (mục 8) nhắm thẳng vào đây — đồng bộ tại chỗ (cùng queryKey): refetchQueries tường minh + keepPreviousData / guard chống ghi đè cũ + retry ngắn / bật refetch theo visibilitychange + repaint nudge.

4 Vấn đề nằm ở đâu trong workflow?

Lỗi xuất hiện ở "khe" giữa bước 8 (ghi vào writer) và bước 12 (đọc lại từ reader). Hai khiếm khuyết cộng hưởng.

① Khiếm khuyết FRONTEND — yếu tố quyết định

Không phòng vệ trước refetch & không tự đồng bộ/repaint lại

  • State lạc quan dataSourceFilter bị ghi đè vô điều kiện bằng kết quả refetch (kể cả khi refetch thiếu shift vừa lưu).
  • Refetch sau lưu chỉ chạy một lần, skipInvalidate:true bỏ nhịp invalidate dự phòng, refetchOnWindowFocus:false → không có đường tự khôi phục.
  • queryKey theo ngày không đổi + WebKit có thể không repaint → màn kẹt đến khi reload/đổi ngày.
② Khiếm khuyết HẠ TẦNG — kích hoạt phụ (đã hạ cấp)

Ghi/đọc 2 endpoint khác nhau, không đảm bảo đọc-sau-ghi

  • Ghi → ShiftRosterPrimaryDao (writer); đọc → ShiftRosterSecondaryDao (reader replica cluster-ro).
  • Refetch ngay sau commit có thể rơi vào cửa sổ lag → đọc dữ liệu cũ.
  • Nhưng lag đo thực chỉ ~15ms (mục 📊) → không đủ gây "kẹt"; chỉ là rủi ro nền.
🔎
Quan hệ nhân quả: ② chỉ là điều kiện kích hoạt thoáng qua (vài chục ms, hiếm trúng); ① mới biến nó thành trạng thái "kẹt" vì không tự đồng bộ/repaint lại. Sửa ① là đủ để hết triệu chứng — và đó chính là giải pháp S3 ở mục 8.

5 Quá trình điều tra (đã thực hiện)

Tóm tắt những gì đã rà soát và cách giả thuyết tiến triển — để kết luận ở mục 6 là kết quả truy vết qua mã nguồn + số liệu thực, không phải phỏng đoán.

Phạm vi đã rà soát

Frontend — caremaker-horizon
  • Luồng Quick Edit: onSubmitQuickEditmutateAsynconFinishQuickEdit.
  • React Query: invalidateShiftCalendarQueries, skipInvalidate, refetchOnWindowFocus, hình dạng queryKey.
  • Effect ghi đè dataSourceFilter; layout timetable transform/sticky/React.memo.
Backend — caremaker-alohomora
  • Tách @AlohomoraMysqlPrimary/Secondary (writer/reader).
  • bulkUpsertShifts ghi đồng bộ trong transaction qua primary.
  • searchWithUnassigned đọc qua secondary (reader).
Hạ tầng + số liệu thực
  • Cluster alohomora-release-rds (2 instance) vs beta (1 instance).
  • CloudWatch: replica-lag & throughput thực (mục 📊).
  • Loại trừ Redis/CDN/SQS/ghi bất đồng bộ (mục ◆ Kiến trúc).

Diễn tiến giả thuyết

  1. Giả thuyết ban đầu: replica-lag (D)
    Ghi vào writer, refetch ngay đọc reader → nghi đọc trúng dữ liệu cũ do replication lag.
  2. Đo số liệu thực → hạ cấp D
    CloudWatch cho thấy lag chỉ ~15ms (đỉnh 49ms), lưu lượng <1 commit/s → khe lag quá hẹp & hiếm trúng, không đủ tạo trạng thái "kẹt tới khi reload". Replica-lag bị hạ xuống rủi ro nền.
  3. Thông tin hiện trường → chuyển trọng tâm sang client
    "Safari/iPad · 1 user · 2 lần · reload hoặc đổi ngày đều khỏi" là chữ ký của lỗi render/refetch phía client, không phải lỗi hạ tầng hệ thống → nổi lên 2 giả thuyết A (paint WebKit) / B (refetch lỡ).
  4. Đọc mã nguồn xác nhận điều kiện client
    Tìm thấy đủ điều kiện cho A/B: ghi đè dataSourceFilter vô điều kiện, skipInvalidate:true, refetchOnWindowFocus:false, queryKey không đổi, layout transform/sticky + React.memo (xem bảng 🔎 Bằng chứng).
⚖️
Giới hạn điều tra (trung thực): lỗi cực hiếm nên chưa bắt được lúc đang xảy ra trên iPad; vì vậy chỉ bằng đọc code + số liệu chưa chốt chắc A hay B — cả hai đều khớp dữ kiện. Đây là lý do giải pháp được chọn (S3) phủ cả hai mà không cần chốt (mục 7–8).

6 Phân tích nguyên nhân & dẫn chứng

Tầng kích hoạt & tầng khuếch đại, kèm số liệu thực đo, khác biệt môi trường và bảng truy vết mã nguồn (các mục hỗ trợ ◆/📊/🔎 ngay dưới).

5.1 — Tầng kích hoạt: Read-after-write trên Aurora Replica

// Luồng GHI — đi tới WRITER (primary) ShiftCalendarModifyService.bulkUpsertShifts() → transactionalOperator.executeAndAwait { batchModifyShiftRosters(...) } // commit ở WRITER (batchModifyShiftRosters là method private của service) → ShiftRosterModifyService.bulkUpsertShiftsForQuickEdit / bulkDeleteShifts / optimizeShiftRosters → ShiftRosterPrimaryDao // @AlohomoraMysqlPrimary // Luồng ĐỌC (refetch ngay sau đó) — đi tới READER REPLICA (secondary) ShiftCalendarInfoService → ShiftRosterInfoService.searchWithUnassigned() → ShiftRosterSecondaryDao // @AlohomoraMysqlSecondary → cluster-ro (readOnly) // ⏱️ Khe lag: WRITER đã có shift mới, REPLICA thì CHƯA → đọc ra dữ liệu cũ

Vì Frontend refetch gần như tức thì sau khi POST trả 200, truy vấn đọc rơi vào cửa sổ replication lag. Replica trả về snapshot cũ — không có shift vừa di chuyển (hoặc shift cũ đã chuyển trạng thái nhưng bản mới chưa replicate) → kết quả nhìn thấy là shift biến mất.

5.2 — Tầng khuếch đại: State race ở Frontend

// tools.tsx — onSubmitQuickEdit() await quickEditMutation.mutateAsync(...); // onSuccess đã invalidate + refetch (đọc reader, có thể cũ) Alert.show("success", ...); onFinishQuickEdit(); // → onQuickEditChange(false, { skipInvalidate: true }) // index.tsx — effect đồng bộ ghi đè state hiển thị useEffect(() => { if (filteredDataSource.length === 0) return; setDataSourceFilter((prev) => { if (isSame(prev, filteredDataSource)) return prev; return merged; // ghi đè bằng dữ liệu refetch — nếu refetch cũ → mất shift }); }, [filteredDataSource, ...]); // chỉ chạy khi filteredDataSource đổi; queryKey(ngày) không đổi → không có nhịp sửa lại
🧠
Đây là khu vực mã đã bị sửa/revert nhiều lần — lịch sử git: fix shift submit cache invalidationRevert "Fix shift submit refetch ordering"Revert "Revert ...". Đó là dấu hiệu kinh điển của một vấn đề thứ tự refetch / nhất quán đọc-ghi chưa được trị tận gốc — khớp với kết luận ở trên.

Khác biệt môi trường DB · dẫn chứng cho mục 6

Phần này mô tả khác biệt writer/reader giữa các môi trường. Với thông tin hiện trường mới (Safari/iPad, cực hiếm), replica-lag đã được hạ cấp — giữ lại như rủi ro nền cho các luồng ghi-rồi-hiển-thị khác.

Môi trườngSố instance AuroraPrimary (ghi)Secondary (đọc)Replication lagTái hiện lỗi?
Beta1 (db.t4g.medium, single-AZ) …cluster-……cluster-ro-… → trỏ về cùng 1 node (không có replica thật) ~0 msKhông
Production (release)2 (writer + reader) …cluster-……cluster-ro-… (replica thật) ~15ms TB · đỉnh 49ms (đo thực)Trên lý thuyết
📌
production/backend/main.tf:33: rds_instance_count=2 (cluster caremakerdb-production — khác cluster alohomora-release-rds của app; xem lưu ý ở bảng bằng chứng #6).
📌
Cluster alohomora-beta-rds-cluster chỉ có 1 instance Aurora (db.t4g.medium): tuy vẫn có reader endpoint cluster-ro nhưng nó trỏ về chính node writer duy nhất → đọc-sau-ghi luôn nhất quán → không thể tái hiện trên beta. (Local/test còn dễ hơn: primary & secondary cùng trỏ localhost.)
💡
Ngoài việc có replica, Production còn chạy 2 ECS task CRM + 2 task Horizon sau ALB (theo đối chiếu hạ tầng) và độ trễ mạng cao/biến thiên hơn beta. Tuy nhiên, với số liệu thực (lag ~15ms, lưu lượng <1 commit/s — mục 📊), khe lag là cực hẹp và hiếm trúng; đây chỉ là rủi ro nền, không phải nguyên nhân chính của ca Safari/iPad mà khách báo.

📊 Số liệu thực đo từ CloudWatch (production)

Đo trực tiếp trên cluster alohomora-release-rds (writer instance-1 + reader instance-1-ap-northeast-1c, cùng AZ ap-northeast-1c). Nguồn: AWS CloudWatch AWS/RDS, profile read-only · cửa sổ 05–19/06/2026 (lag) & 12–19/06/2026 (throughput). Mục đích: lượng hóa replica-lag & lưu lượng để kiểm chứng/loại trừ các giả thuyết.

~15 ms
Replica lag trung bình
28–49 ms
Lag đỉnh/ngày (max 49ms · 12/06)
0.7 /s
Commit (TB) · đỉnh ~20/s
~14
Kết nối DB đồng thời (TB) · đỉnh 25–42

Replica lag (reader cluster-ro)

Chỉ sốGiá trị
Lag trung bình (14 ngày)~15.2–15.9 ms
Lag đỉnh theo ngày28–49 ms
Lag đỉnh tuyệt đối49 ms (12/06/2026)

Lag ổn định & rất nhỏ — không có spike bất thường trong cả 14 ngày.

Lưu lượng ghi/đọc (writer)

Chỉ sốTrung bìnhĐỉnh
Queries/s~20~315
DML (ghi)/s~1.4~97
Commit/s~0.7~20
Kết nối đồng thời13–1525–42

Hệ thống lưu lượng thấp: trung bình <1 commit/giây, hiếm khi đông người dùng đồng thời.

🎯
Số liệu xác nhận: replica-lag KHÔNG thể là nguyên nhân của triệu chứng "kẹt". Lag thực chỉ ~15ms (đỉnh 49ms). Sau khi POST trả 200, một lần refetch còn mất thêm round-trip mạng + xử lý backend (hàng chục ms) — tới lúc đọc reader thì lag ~15ms gần như đã trôi qua. Quan trọng hơn: kể cả nếu một lần đọc rơi đúng khe lag, lần render kế tiếp (≤50ms sau) đã có data đúng — không thể giải thích trạng thái kẹt kéo dài tới khi reload/đổi ngày. Persistence này chỉ có thể đến từ tầng client không tự đồng bộ lại (state race / paint WebKit).
🔁
Lưu lượng thấp khớp với độ hiếm "1 user · 2×". Trung bình <1 commit/s và lag đỉnh chỉ 49ms ⇒ xác suất một refetch rơi trúng khe lag là cực nhỏ; và ngay cả khi trúng cũng tự khỏi trong ~50ms. Điều này vừa giải thích vì sao bug cực hiếm, vừa loại trừ replica-lag khỏi vai trò nguyên nhân chính — củng cố giả thuyết A/B phía client (Safari/iPad).
🏗️
Đính chính cluster (đã đối chiếu CloudWatch): app alohomora nối tới cluster alohomora-release-rds (2 instance: 1 writer + 1 reader, cùng AZ 1c, db.t3.large, MultiAZ=false). Cluster caremakerdb-production trong caremaker-infracluster khác — bằng chứng rds_instance_count=2 ở mục 8 chỉ chứng minh mẫu hình writer/reader, không phải đúng cluster của app. Cả hai cluster đều có 2 members (đã xác nhận qua describe-db-clusters).

Tác động tới phương án tái hiện

Client (A/B) — ưu tiên

Vì lag thực quá nhỏ, không thể tái hiện bằng cách đua replica-lag tự nhiên; và lỗi cực hiếm nên cũng khó ép tái hiện trên iPad. Hướng thực tế: fix phòng vệ S3 (mục 8) + telemetry nền để bắt lần kế tiếp tự động; soi Web Inspector chỉ là bước tùy cơ (mục 7).

Nếu vẫn muốn kiểm chứng giả thuyết lag

Phải bơm lag nhân tạo (ép đọc reader ngay sau ghi với độ trễ giả lập) hoặc chạy refetch trong vòng lặp gắt sát thời điểm commit — vì lag tự nhiên ~15ms gần như không bao giờ lọt vào cửa sổ render. Đây là kiểm chứng học thuật, không phản ánh tải thực.

🔎 Bằng chứng mã nguồn & cấu hình · dẫn chứng cho mục 6

Bảng truy vết để đội phát triển kiểm chứng nhanh.

#Vị tríVai trò trong lỗi
1caremaker-alohomora/lib/mysql-base/.../application-mysql.ymlKhai báo primary (cluster) & secondary (cluster-ro, readOnly) cho production → tách writer/reader.
2mysql-base/.../MysqlConfiguration.ktĐịnh nghĩa 2 bean datasource: @AlohomoraMysqlPrimary & @AlohomoraMysqlSecondary.
3mysql-base/.../dao/ShiftRosterDao.ktSealed class: ShiftRosterPrimaryDao (dòng 45, ghi) & ShiftRosterSecondaryDao (dòng 1318, đọc — chứa searchWithUnassigned).
4shift-core/.../ShiftRosterInfoService.kt:45searchWithUnassigned gọi shiftRosterSecondaryDao → xác nhận luồng đọc timetable đi qua reader replica.
5app/crm/.../ShiftCalendarModifyService.ktbulkUpsertShifts: ghi đồng bộ qua primary/writer trong transaction (qua lớp ShiftRosterModifyService); trả ShiftUpdateResponse(isSucceed=true) (không kèm dữ liệu shift mới).
6caremaker-infra/production/backend/main.tf:33rds_instance_count=2 → mẫu hình writer+reader trên production. ⚠ Lưu ý: cluster này là caremakerdb-production (database_id="caremakerdb", main.tf:28), không phải cluster alohomora-release-rds mà app alohomora thực sự kết nối — cluster alohomora không được định nghĩa trong caremaker-infra. Đây là bằng chứng gián tiếp cho mẫu hình, không phải cho đúng cluster của app.
7caremaker-horizon/hook/useShift.ts:746useBulkUpsertShifts.onSuccessinvalidateShiftCalendarQueries → refetch ngay (đọc reader).
8caremaker-horizon/.../shift/index.tsx:920Effect ghi đè dataSourceFilter bằng kết quả refetch (không phòng vệ dữ liệu cũ).
9caremaker-horizon/.../shift/index.tsx:1104onFinishQuickEdit → onQuickEditChange(false,{skipInvalidate:true}): bỏ nhịp invalidate dự phòng.
10caremaker-horizon/.../partials/tools.tsx:1166onSubmitQuickEdit: trình tự submit → mutate → finish.
11caremaker-horizon/.../shift/ShiftTimetable.tsx:159CLIENT Cột ảo hóa định vị bằng transform: translateX → rủi ro repaint WebKit (giả thuyết A).
12ShiftTimetable.tsx:184-232CLIENT React.memo so sánh chủ yếu bằng reference cho cột staff.
13ShiftTimetable.tsx:505-516CLIENT Vá sẵn "avoid momentary blanking / width collapse" → lớp lỗi render đã tồn tại.
14wrappers.tsx:158 · partials/index.tsx:496CLIENT position: sticky chồng trên ancestor có transform (rủi ro paint iOS).
15ManagementLayoutWrapper.tsx:21CLIENT refetchOnWindowFocus:false → mất đường khôi phục refetch (giả thuyết B).
16hook/useWindowDimension.ts:17,26CLIENT isMobile theo innerWidth ≤ MOBILE_BREAKPOINT (767) → iPad chạy layout desktop dưới WebKit cảm ứng.

7 Phương pháp tái hiện & chẩn đoán (Safari / iPad)

Lỗi cực hiếm (1 user · 2 lần) nên gần như không ép tái hiện được theo ý muốn. Do đó chiến lược thực tế là: không chờ chẩn đoán mà triển khai (1) một fix phòng vệ phủ cả hai giả thuyết + (2) telemetry chạy nền để xác nhận sau; việc soi Web Inspector chỉ là cơ hội nếu may bắt được lỗi đang xảy ra.

KHI KHÔNG TÁI HIỆN ĐƯỢC (đường đi thực tế — khuyến nghị): Đừng để việc "phải tái hiện" chặn tiến độ. Vì reload hoặc đổi ngày đều khỏi, hãy triển khai ngay:
  • Sửa "mù" bằng S3 — đồng bộ lại tại chỗ khi thoát quick-edit (refetch cùng key + keepPreviousData, guard chống ghi đè cũ + retry ngắn, repaint nudge). Trị cả A lẫn B nên không cần chốt giả thuyết, và không nháy/không reset scroll (KHÔNG bump queryKey/không remount — xem cảnh báo UX ở mục 8).
  • Telemetry chạy nền trên production (log thời điểm refetch, độ dài kết quả search, visibilitychange) — bắt lần xảy ra kế tiếp tự động, không cần cắm iPad chờ.
→ Xem lộ trình ở mục 8.
0️⃣
BƯỚC 0 — Chẩn đoán CƠ HỘI (không bắt buộc, không chặn việc sửa): Nếu tình cờ bắt được lỗi đang xảy ra trên một iPad cụ thể, hãy tận dụng: kết nối iPad với Mac, mở Safari → Develop → [iPad] → Web Inspector, soi DOM & cache theo cây quyết định ngay dưới để chốt A hay B. Lưu ý số liệu: lag thực chỉ ~15ms (mục 📊) nên không thể tái hiện bằng đua replica-lag tự nhiên. Vì không chủ động tái hiện được, đây chỉ là bước tùy cơ — telemetry nền (ở trên) mới là cách bắt lỗi đáng tin.
Giả thuyết A — Lỗi paint của WebKit

Ảo hóa cột bằng transform + sticky → không repaint sau khi data đổi

  • Mỗi cột staff định vị tuyệt đối bằng transform: translateX(start)ShiftTimetable.tsx:159.
  • Cột trái & header dùng position: sticky chồng trên ancestor có transform — partials/wrappers.tsx:158, partials/index.tsx:496.
  • Container cuộn lồng nhau, overscrollBehavior:none, momentum scroll — partials/wrappers.tsx:126.
  • Không có GPU repaint hint (translateZ(0) / will-change / backface-visibility).
  • React.memo so sánh chủ yếu bằng referenceShiftTimetable.tsx:184-232.
🩹
Đội đã phải vá "avoid momentary blanking" & "avoid width collapse"ShiftTimetable.tsx:505-516. Chứng tỏ lớp lỗi render/biến mất này đã tồn tại sẵn trên màn này.
Giả thuyết B — Refetch bị bỏ lỡ, không khôi phục

iOS treo fetch khi mất foreground + tắt refetch-on-focus

  • iOS/iPadOS Safari hay tạm dừng/hủy fetch & JS khi trang chớp mất foreground (toast, cử chỉ hệ thống, chuyển app).
  • Refetch sau lưu phụ thuộc duy nhất vào invalidate của mutation; nhánh thoát dùng skipInvalidate:true → bỏ nhịp invalidate dự phòng — index.tsx:1104.
  • refetchOnWindowFocus:false → quay lại trang không tự fetch lại — ManagementLayoutWrapper.tsx:21.
  • ⇒ Nếu refetch bị lỡ, cache giữ data cũ → kẹt đến khi reload/đổi ngày.
📱
iPad bị nhận diện là "desktop": isMobile = window.innerWidth ≤ MOBILE_BREAKPOINT (767) (useWindowDimension.ts:17,26). iPad (≥768px) chạy layout desktop transform/sticky đầy đủ nhưng dưới compositor WebKit cảm ứng — chính sự lệch pha này là nơi bug paint iOS (giả thuyết A) thường phát sinh.

Cây quyết định chẩn đoán (làm trên đúng iPad, qua Safari Web Inspector)

Tái hiện lỗi → lúc shift "biến mất", mở Web Inspector soi DOM của cột staff
Node shift CÓ trong DOM nhưng không vẽ
opacity/transform/visibility bình thường, vẫn không thấy
⇒ Kết luận A — Lỗi paint WebKit

Fix: thêm repaint nudge (translateZ(0)/will-change:transform cho cột, hoặc ép reflow sau khi data đổi); cân nhắc tách sticky khỏi ancestor có transform.

Node shift KHÔNG có trong DOM
kiểm cache React Query useShiftTimetable
⇒ Kết luận B/C — Refetch lỡ / State race

Nếu cache cũ ⇒ refetch bị lỡ (B). Fix: đảm bảo refetch chắc chắn khi thoát quick-edit (bỏ skipInvalidate hoặc refetchQueries tường minh) + bật refetch theo visibilitychange/focus.

Mẹo bổ sung: khi đang "kẹt", thử vuốt nhẹ cuộn timetable — nếu shift hiện ra ngay khi scroll mà chưa fetch lại, đó là bằng chứng mạnh cho A (lỗi paint).

⚖️
Lưu ý trung thực: chỉ bằng đọc mã nguồn không thể chốt chắc chắn A hay B — cả hai đều khớp với "Safari iPad · hiếm · reload/đổi ngày khỏi". Phần hạ tầng AWS (mục ◆ Kiến trúc & dẫn chứng mục 6, replica-lag) vẫn là rủi ro có thật cho các luồng khác, nhưng không còn là nghi phạm chính cho hiện tượng cụ thể mà khách báo.

8 Giải pháp khắc phục

Vì chẩn đoán trực tiếp gần như bất khả thi (mục 7), ưu tiên giải pháp phòng vệ phủ cả hai giả thuyết — S3 — không phụ thuộc việc chốt A/B. S1/S2 chỉ áp bổ sung nếu sau này telemetry/Web Inspector chốt được nguyên nhân.

★ GIẢI PHÁP KHUYẾN NGHỊ — phủ cả A & B, KHÔNG ảnh hưởng UX

S3. Đồng bộ lại tại chỗ khi thoát quick-edit (không remount, không đổi queryKey)

Mục tiêu: đạt đúng kết quả của thao tác "đổi ngày" (dữ liệu mới + vẽ lại đúng) nhưng làm tại chỗ nên không chớp trắng, không mất vị trí cuộn, không double-fetch giật. Gồm 3 việc nhẹ, đều giữ nguyên queryKey:

  1. Refetch tin cậy cùng queryKey khi thoát quick-edit (bỏ skipInvalidate hoặc refetchQueries(["useShiftTimetable"]) tường minh). Đặt keepPreviousData:truedata cũ vẫn hiển thị trong lúc tải → không nháy, không reset scroll. → đảm bảo luôn có một lần đọc mới (trị gốc B).
  2. Guard + retry ngắn (xem P2 & P1): chỉ ghi kết quả refetch vào dataSourceFilter khi nó đã chứa shift vừa lưu; nếu chưa (trúng khe lag) thì retry vài nhịp ~1s rồi mới đồng bộ. → không bao giờ "nháy mất shift", trị B chắc chắn kể cả khi lag.
  3. Repaint nudge tại chỗ (xem S1): sau khi dataSourceFilter đổi, một nhịp requestAnimationFrame + ép reflow (đọc offsetHeight) hoặc toggle will-change trên cột. → ép WebKit vẽ lại (trị A) mà KHÔNG dựng lại bảng.
⚠️
Tránh: KHÔNG dùng cách bump version vào queryKey hay remount component bằng key — nó tạo query mới (cache rỗng) ⇒ chớp trắng + reset scroll sau mỗi lần lưu (đúng lớp lỗi "avoid momentary blanking" mà đội đã từng phải vá). Đồng bộ tại chỗ ở trên cho cùng kết quả mà không có chi phí UX đó.
Thành phần — repaint cho A (cũng dùng độc lập nếu đã chốt A)

S1. Ép repaint tại chỗ sau khi data đổi

Thêm GPU hint cho cột (will-change:transform / translateZ(0)) hoặc ép reflow một nhịp (rAF + đọc offsetHeight) sau khi dataSourceFilter đổi. Cân nhắc tách sticky khỏi ancestor có transform. Không remount ⇒ không ảnh hưởng UX.

Thành phần — refetch tin cậy cho B (cũng dùng độc lập nếu đã chốt B)

S2. Refetch tin cậy + đường khôi phục

Bỏ skipInvalidate / refetchQueries(["useShiftTimetable"]) tường minh — cùng queryKey + keepPreviousData nên không nháy/không reset scroll. Thêm refetch theo visibilitychange/focus để iOS quay lại foreground là tự đồng bộ (lưới an toàn cho iOS background).

Chi tiết 2 thành phần của S3 (đều tại chỗ — không ảnh hưởng UX)

Thành phần S3 — chống "nháy mất shift"

P2. Guard: không ghi đè bằng dữ liệu cũ

Trong effect đồng bộ (index.tsx:920), không ghi đè dataSourceFilter bằng kết quả refetch nếu refetch chưa chứa thay đổi vừa lưu; chỉ thay khi đã khớp. Giữ state lạc quan đang đúng ⇒ không nháy. (P3 cũ trùng S3 đã gộp.)

Thành phần S3 — bù replica-lag

P1. Retry ngắn đến khi dữ liệu phản ánh thay đổi

Nếu refetch đầu chưa chứa shift vừa lưu (trúng khe lag ~15ms), retry có backoff ngắn (vài lần trong ~1–2s) rồi mới đồng bộ. Chạy ngầm, không chặn UI (data cũ vẫn hiển thị) ⇒ người dùng không thấy gì bất thường.

Phương án nền & quan trắc

Backend — triệt để (backlog)

B1. Đọc-lại-sau-ghi từ Primary (writer)

Với truy vấn timetable ngay sau thao tác ghi, ép đọc từ primary thay vì secondary (ví dụ tham số consistentRead, hoặc để bulkUpsert trả thẳng dữ liệu mới đọc từ writer). Loại bỏ tận gốc khe lag cho mọi luồng ghi-rồi-hiển-thị.

Đánh đổi: tăng tải writer (chấp nhận được vì tần suất thấp). Không khẩn cho ca này.

Quan trắc — bắt lỗi hiếm

T1. Telemetry chạy nền (production)

Log thời điểm refetch, độ dài kết quả search, sự kiện visibilitychange + lỗi/hủy fetch. Không cần tái hiện thủ công — bắt tự động lần xảy ra kế tiếp để xác nhận đã hết hoặc chốt A/B.

Tổng hợp ưu / nhược điểm & đề xuất

Phương ánƯu điểmNhược điểmĐề xuất
S3
đồng bộ tại chỗ
Phủ cả A + B + D; không cần chốt nguyên nhân; UX-safe (không nháy/không reset scroll); client-only, blast radius hẹp; triển khai nhanh. Là tổ hợp 3 việc → cần làm đủ & cẩn thận từng phần; repaint nudge mang tính workaround WebKit (có thể phải tinh chỉnh). ✅ Nên làm ngay
Giải pháp chính
S1
repaint nudge
Trị trực tiếp A (paint); tại chỗ, không chi phí UX; rẻ. Workaround mức trình duyệt (translateZ/will-change/reflow), phụ thuộc hành vi WebKit; không trị B một mình. 🧩 Thành phần S3
Dùng độc lập nếu chốt A
S2
refetch tin cậy
Đảm bảo có refetch sau lưu (trị B); visibilitychange phủ iOS background; cùng key + keepPreviousData nên không nháy. Refetch theo focus/visibility có thể thêm request khi user chuyển tab nhiều (tải nhẹ); không trị A một mình. 🧩 Thành phần S3
Dùng độc lập nếu chốt B
P2
guard ghi đè
Chặn ngay triệu chứng "nháy mất shift" kể cả khi refetch trả dữ liệu cũ; rẻ, logic rõ. Cần tiêu chí "đã chứa shift vừa lưu" chính xác (so khớp id/nội dung); sai tiêu chí có thể giữ state cũ lâu hơn cần thiết. 🧩 Thành phần S3
P1
retry ngắn
Bù replica-lag thông thường; chạy nền, không chặn UI (data cũ vẫn hiển thị). Thêm vài request; phải giới hạn số lần/timeout tránh loop; vô ích nếu server thật sự chưa có dữ liệu (không phải lag). 🧩 Thành phần S3
Giới hạn 2–3 lần ~1–2s
T1
telemetry
Bắt lỗi hiếm tự động, không cần tái hiện; xác nhận đã hết hoặc chốt A/B; giá trị quan trắc lâu dài. Cần hạ tầng log/dashboard; thêm chút payload; không tự sửa bug (chỉ quan sát). 📡 Làm song song
Để verify, không phải fix
B1
đọc từ primary
Loại tận gốc khe lag cho mọi luồng ghi-rồi-hiển; đúng đắn về nhất quán đọc-sau-ghi. Không trị A/B (nghi phạm chính) → một mình gần như không hết bug này; blast radius rộng; dồn tải reader→writer; phức tạp BE; khó kiểm chứng đã fix. ⏳ Backlog hạ tầng
Scope hẹp; KHÔNG cho ca này
Lộ trình đề xuất (thực dụng — sửa trước, xác nhận sau):
  1. Triển khai ngay S3 = đồng bộ tại chỗ gồm 3 việc: refetch cùng key + keepPreviousData (S2) · guard chống ghi đè cũ + retry ngắn (P2+P1) · repaint nudge tại chỗ (S1). Phủ cả A & B, không cần chốt nguyên nhânkhông ảnh hưởng UX (không nháy, không reset scroll).
  2. Bật T1 — telemetry nền để bắt lần xảy ra kế tiếp tự động (thay cho việc cố tái hiện trên iPad).
  3. Theo dõi: nếu telemetry không còn ghi nhận sự cố ⇒ coi như đã khỏi. Nếu vẫn còn, dữ liệu telemetry sẽ chốt A/B ⇒ tăng cường đúng thành phần (vd siết repaint cho A).
  4. Backlog hạ tầng: giữ B1 để xử lý rủi ro replica-lag cho các luồng ghi-rồi-hiển-thị khác (không khẩn cho ca này).

Tiêu chí nghiệm thu

Vì lỗi không ép tái hiện được, tiêu chí được đặt theo hướng kiểm chứng tất định hành vi của fix (không phụ thuộc việc làm lỗi xuất hiện).

📖 Bảng thuật ngữ

Giải thích ngắn gọn các thuật ngữ kỹ thuật dùng trong tài liệu. Mẹo: mọi thuật ngữ xuất hiện trong phần văn bản đều có gạch chân nét đứt — rê chuột (hover) lên để xem nhanh định nghĩa.

Frontend · React / React Query

Thuật ngữÝ nghĩa
mutationTrong React Query: thao tác ghi/thay đổi dữ liệu lên server (tạo/sửa/xóa), đối lập với query (đọc). "mutation shift" = mutation cho dữ liệu ca.
queryTruy vấn đọc dữ liệu, được React Query cache lại theo queryKey.
queryKeyKhóa định danh một query trong cache. Đổi queryKey ⇒ React Query coi là truy vấn mới và fetch lại.
cacheBộ nhớ tạm phía client lưu kết quả query để dùng lại, tránh gọi server liên tục.
staleTrạng thái dữ liệu cache bị coi là "cũ/hết tươi" — cần refetch khi có cơ hội (mount lại, focus, hoặc bị invalidate).
invalidate / invalidateQueriesĐánh dấu query là stale; với query đang hiển thị (active) thì kích hoạt refetch ngay.
invalidateShiftCalendarQueriesHàm bao dùng chung của dự án: gom & invalidate 5 nhóm queryKey lịch cùng lúc, bọc try/catch nên lỗi refetch không làm mutation thất bại.
refetchTải lại dữ liệu từ server cho một query.
refetchOnWindowFocusTùy chọn React Query: tự refetch khi tab/cửa sổ được focus lại. Dự án đặt false.
skipInvalidateCờ nội bộ: khi thoát Quick Edit thì bỏ qua lần invalidate dự phòng (vì mutation đã invalidate rồi).
optimistic / state lạc quanCập nhật UI ngay theo thao tác người dùng trước khi server xác nhận, để cảm giác mượt.
stateTrạng thái dữ liệu cục bộ trong một component React; đổi state ⇒ render lại.
effect / useEffectHàm chạy sau render để đồng bộ tác động phụ (vd: copy dữ liệu server vào state cục bộ).
React.memoBọc component để bỏ qua re-render nếu props không đổi (so sánh bằng reference).
reference (so sánh tham chiếu)So sánh hai giá trị bằng địa chỉ bộ nhớ, không so sánh sâu nội dung. Khác với deepEqual.
deepEqualSo sánh sâu toàn bộ nội dung hai đối tượng (chậm hơn nhưng chính xác).
renderQuá trình React dựng/ cập nhật giao diện từ state & props.

Trình duyệt · CSS · Hiển thị

Thuật ngữÝ nghĩa
WebKitEngine trình duyệt của Safari (iOS/iPadOS/macOS). Có một số đặc thù về vẽ lớp khác Chrome/Firefox.
compositing / repaintQuá trình trình duyệt ghép & vẽ lại các lớp đồ họa lên màn hình. Lỗi compositing = DOM đã đổi nhưng màn hình chưa vẽ lại.
reflowTrình duyệt tính lại layout (vị trí/kích thước) các phần tử.
transform / translateXBiến đổi CSS dịch chuyển phần tử (thường chạy trên GPU). Dùng để định vị cột timetable.
sticky (position)Phần tử "dính" tại mép khi cuộn (vd cột trái / header bảng).
will-change / translateZ(0)Gợi ý cho trình duyệt tạo lớp vẽ riêng / dùng GPU, thường để ép repaint đúng lúc.
virtualization (ảo hóa)Chỉ render phần đang nhìn thấy (cột/hàng), giúp bảng lớn mượt hơn.
overscrollBehaviorThuộc tính CSS kiểm soát hành vi cuộn vượt biên (chặn lan ra phần tử cha).
momentum scrollCuộn quán tính (đặc trưng iOS) — vuốt rồi vẫn trôi tiếp.
visibilitychangeSự kiện khi tab/trang chuyển ẩn ↔ hiện; dùng để biết lúc quay lại foreground.
foreground / background (iOS)App đang hiển thị / bị ẩn. iOS hay tạm dừng JS & hủy fetch khi trang rời foreground.
userAgentChuỗi trình duyệt tự khai báo về thiết bị/OS; dùng để nhận diện iPad.
breakpointNgưỡng độ rộng màn hình để phân biệt mobile/desktop (dự án: 767px).
long-pressThao tác nhấn giữ trên màn cảm ứng (kích hoạt kéo–thả).
DOMCây phần tử trang web mà trình duyệt dựng từ HTML; React cập nhật DOM khi state đổi.

Backend · Cơ sở dữ liệu

Thuật ngữÝ nghĩa
writer / primaryNode CSDL nhận ghi (master). Mọi thay đổi commit ở đây trước.
reader / secondary / replicaBản sao chỉ đọc của CSDL, dữ liệu được sao chép bất đồng bộ từ writer.
replication lag / replica-lagĐộ trễ sao chép dữ liệu từ writer sang reader (đo thực ~15ms, đỉnh 49ms).
read-after-write (đọc-sau-ghi)Đọc lại ngay sau khi ghi. Nếu đọc trúng reader đang lag ⇒ có thể nhận dữ liệu cũ.
transactionGiao dịch CSDL: nhóm thao tác hoặc thành công toàn bộ hoặc hủy toàn bộ (rollback).
bulk upsertGhi hàng loạt: bản ghi nào chưa có thì chèn, đã có thì cập nhật.
DAOData Access Object — lớp mã chuyên truy cập dữ liệu (đọc/ghi DB).
R2DBCReactive Relational Database Connectivity — truy cập CSDL kiểu reactive (non-blocking) trên JVM/Kotlin.
transactionalOperator / executeAndAwaitCơ chế chạy một khối lệnh bên trong transaction theo phong cách reactive.
sealed class(Kotlin) Lớp niêm phong — giới hạn tập lớp con cho phép (vd Primary/Secondary DAO).
AuroraDịch vụ CSDL quan hệ của AWS, tương thích MySQL; tự tạo writer endpoint & reader endpoint.
cluster-roReader endpoint của Aurora (ro = read only) — chỉ phục vụ đọc.

AWS · Hạ tầng · Khác

Thuật ngữÝ nghĩa
ECS FargateChạy container không cần quản lý server trên AWS; mỗi "task" là một instance ứng dụng.
ALBApplication Load Balancer — cân bằng tải HTTP tới nhiều task.
CloudFrontCDN của AWS — phân phối/đệm nội dung gần người dùng.
WAFWeb Application Firewall — tường lửa lọc request web độc hại.
Secrets ManagerDịch vụ AWS lưu bí mật/cấu hình (vd host & mật khẩu DB).
CloudWatchDịch vụ giám sát & metrics của AWS (nguồn số liệu replica-lag, throughput ở mục 📊).
ElastiCache / Redis / ValkeyCache trong bộ nhớ (in-memory). Ở dự án chỉ cache auth/session, không cache kết quả search shift.
polling / backoffHỏi lại server nhiều lần, khoảng cách tăng dần, đến khi đạt điều kiện.
telemetryGhi log/đo đạc hành vi để chẩn đoán sự cố hiếm gặp.
recurrence (今回のみ / 今後すべて)Phạm vi áp dụng thay đổi ca: chỉ lần này / từ nay về sau.