Phân tích luồng sau chuyển — batch/job-daily-update-status — vòng 1
logic-reviewerviết dựa trên code đã implement (không phải thiết kế trong spec), đối chiếu với code cũ. Dev đọc file này cùngchanges.diff: mỗi luồng cho biết code mới đi qua những đâu, khác code cũ ở bước nào, và chỗ nào cần đọc kỹ.
Viết tắt: BMS = easyposbackend1:src/main/java/vn/softdreams/easypos/service/impl/BatchesManagementServiceImpl.java;
SCH = presentation/scheduler/BatchStatusUpdateScheduler.java; SVC = application/batch/BatchStatusUpdateService.java;
REPO = infrastructure/relational/batch/adapter/BatchRelationalRepository.java; WR = infrastructure/relational/batch/adapter/BatchStatusWriter.java;
EW = domain/batch/model/BatchExpiryWarning.java; SP = domain/batch/model/BatchStatusPolicy.java (đường dẫn dưới src/main/java/com/softdreams/easypos/warehouse/).
F-01 Cập nhật trạng thái hạn dùng của lô
- Điểm vào: cũ
BMS:113-128→ mớiSCH:44-57(updateStatusesOfBatches) - Quy tắc: R-01, R-02, R-03, R-04, R-05, R-06
Sơ đồ luồng mới (theo code thực tế)
sequenceDiagram
participant SCH as BatchStatusUpdateScheduler.updateStatusesOfBatches
participant L as DistributedLockManager.acquireLock
participant S as BatchStatusUpdateService.updateStatuses (@Transactional)
participant R as BatchRelationalRepository
participant W as BatchStatusWriter
Note over SCH: cờ app.scheduler.enabled + app.scheduler.batch-status-update.enabled; cron 0 25 0/4 * * ?
SCH->>SCH: try { clientId = BatchExpiryWarningScheduler.clientId()
SCH->>L: acquireLock(job-lock-daily-update-status, clientId, 10m)
L-->>SCH: false → LOG.info(LOCK_KEY), return
SCH->>S: updateStatuses() [proxy, mở transaction]
S->>R: findBatchesWithExpiryDate()
R->>W: findWithExpiryDate(): JPQL SELECT b ... WHERE expDate IS NOT NULL (entity managed) → BatchStatusCandidate
S->>S: today = LocalDate.ofInstant(clock.instant(), systemDefault).atStartOfDay
loop mỗi lô
S->>S: BatchExpiryWarning(unit, value) + BatchStatusPolicy.statusOf (NPE nếu unit null / value null với unit 1-3)
S->>S: status khác batch.status() → thêm BatchStatusChange
end
S->>S: changes rỗng → return (commit rỗng)
S->>R: updateStatuses(changes, ZonedDateTime.ofInstant(clock.instant()))
R->>W: find (cache cấp 1) → setStatus, updater=0, updateTime, LegacyCleanText.clean(code/lotNo/normalizedName)
S-->>SCH: commit → Hibernate UPDATE dynamic dòng dirty
SCH->>SCH: catch RuntimeException → LOG.error (sau rollback)
Đối chiếu từng bước
| # | Luồng cũ (repo:path:dòng) |
Luồng mới (path:dòng) |
Kết luận |
|---|---|---|---|
| 1 | Cron 0 25 0/4 * * ?, chỉ cờ chung app.scheduler.enabled — BMS:113; easyposbackend1:.../config/SchedulerConfiguration.java:9-10 |
SCH:21,44; config/SchedulingConfiguration.java:8-9; application.yml batch-status-update.enabled mặc định false |
KHÁC có chủ đích (D-01) |
| 2 | Transaction cấp class mở trước khoá — BMS:55-57,113-115 |
SCH không có transaction; khoá trước, transaction mở ở SVC:40 |
KHÁC có chủ đích (D-09) |
| 3 | clientId = IPv4 BaseCommon.getIpv4()/"local.server.external"; whitelist config.json — BMS:116,401-407 |
SCH:46 → BatchExpiryWarningScheduler.clientId() (InetAddress.getLocalHost()), không whitelist |
KHÁC có chủ đích (D-02) |
| 4 | SETNX 10 phút, không nhả; null → chạy; có chủ → log chủ khoá — BMS:117-127 |
SCH:47-50, không releaseLock; false → log hằng LOCK_KEY |
KHÁC có chủ đích (D-03) cho nhánh race; log khác: KHÁC (F-logic-1) |
| 5 | findAllByExpDateIsNotNull(), mọi công ty, entity managed — BMS:414 |
SVC:42 → REPO:181-184 → WR:21,31-37 JPQL expDate IS NOT NULL, entity managed |
KHỚP |
| 6 | today = ZonedDateTime.now().toLocalDate().atStartOfDay(systemDefault) sau khi nạp — BMS:415 |
SVC:43-44 từ Clock.systemDefaultZone() sau khi nạp |
KHỚP |
| 7 | getWarningDate: switch Integer (null → NPE), 1/2/3 → ngày/tháng/năm (value null → NPE), khác → 0 — BMS:551-568 |
EW:55-67 (unit == DAY unboxing null → NPE; Period.ofDays(null) → NPE; unit lạ → ZERO kể cả value null) |
KHỚP |
| 8 | today < warningDate → 1; !today.isAfter(exp) → 2; còn lại 0 — BMS:420-426 |
SP:29-32 |
KHỚP |
| 9 | Đọc exp_date ZonedDateTime (Hibernate 5.6, jdbc.time_zone VN, đặt múi JVM) |
WR:59 → BatchDateTimes.toZonedDateTime (Timestamp đọc theo jdbc.time_zone VN của profile dev/uat/prod → instant → múi JVM) |
KHỚP |
| 10 | Set status mọi lô; Hibernate chỉ UPDATE dòng status đổi (Integer.equals, NULL → khác) — BMS:417-429 |
SVC:49-51 lọc !Integer.valueOf(status).equals(batch.status()) |
KHỚP |
| 11 | Lỗi ở lô bất kỳ → không ghi gì (ghi lúc commit) — BMS:409-430 |
Tính hết trước (SVC:46-52), NPE ném trước updateStatuses |
KHỚP |
| 12 | Không dòng đổi → saveAll không UPDATE |
SVC:53-55 return sớm, commit rỗng |
KHỚP |
| 13 | @PreUpdate dòng dirty: updater = auditor (0), update_time = thời điểm flush từng dòng, CleanTextListener trên code/lot_no/normalized_name — easyposbackend1:.../domain/Batches.java:13-16; AbstractAuditingEntity.java:49-59; LF-30, LF-31 |
WR:43-53: status, updater=0, updateTime = một now lấy sau khi tính, LegacyCleanText.clean ba cột |
KHỚP (status/updater/làm sạch); update_time: KHÁC có chủ đích (D-06) |
| 14 | @DynamicUpdate → UPDATE chỉ cột đổi lúc commit |
BatchJpaEntity @DynamicUpdate, không persist/merge/flush tay |
KHỚP (UNVERIFIED tới khi chạy MSSQL thật, K-13) |
| 15 | Lỗi thoát dailyUpdateStatus → Spring log, rollback — BMS:113-128 |
SVC ném qua proxy → rollback; SCH:53-55 bắt RuntimeException, log |
KHÁC có chủ đích (D-10) |
Dữ liệu và side effect
| Bảng / topic / cache | Cũ (đọc/ghi, điều kiện) | Mới (đọc/ghi, điều kiện) | Ghi chú |
|---|---|---|---|
Redis job-lock-daily-update-status |
SETNX = IPv4, TTL 10 phút, không nhả; trong transaction | acquireLock cùng khoá, TTL 10 phút, không nhả; ngoài transaction |
D-02, D-03, D-09; chung Redis với monolith → loại trừ nhau |
batches (đọc) |
mọi cột, mọi công ty, exp_date IS NOT NULL |
như cũ (entity đầy đủ) | D-05 giữ |
batches (ghi) |
UPDATE dòng status đổi: status, updater (nếu khác 0), update_time (flush từng dòng), chuỗi làm sạch nếu khác |
UPDATE dòng status đổi: status, updater (nếu khác 0), update_time (= một now), chuỗi làm sạch nếu khác |
D-06, D-07 |
Transaction, khoá, idempotency, tenant
- Ranh giới transaction: cũ
@Transactionalcấp class bao cả khoá và cập nhật → mới chỉSVC.updateStatuses(SVC:40), gọi qua proxy từSCH:52; khoá ngoài (D-09). Catch ởSCHngoài transaction → rollback vẫn xảy ra (K-03). - Khoá / cập nhật nguyên tử: không khoá dòng, không
@Versionở cả hai bên; ghi đèstatustheo dữ liệu nạp đầu transaction (F-logic-2, nghi-bug-cu, cũ = mới). - Idempotency, thời điểm phát event: không có event. Chạy lại (sau 10 phút hoặc node khác) → status đã đúng, không ghi thêm.
- Lọc
comId: không lọc ở cả hai (job hệ thống, mọi công ty — R-03, D-05). Không có cache. - Truy vấn: một SELECT;
findtrongWR:46lấy từ persistence context, không N+1. Đọc DB chính (transaction không readOnly, K-24).
Lệch so với luồng mới đã duyệt (spec mục 6)
không
Dev cần chú ý khi review
| Vị trí | Vì sao |
|---|---|
WR:46-52 |
Audit + làm sạch chuỗi tự mô phỏng thay listener của monolith; chỉ chạy cho dòng đổi status. Cột thực tế trong câu UPDATE phụ thuộc dirty-check Hibernate 6 + @DynamicUpdate — UNVERIFIED tới khi chạy MSSQL thật. |
WR:49 + application-*.yml hibernate.jdbc.time_zone |
update_time/exp_date dựa vào jdbc.time_zone: Asia/Ho_Chi_Minh có ở profile dev/uat/prod; base application.yml không có. |
SVC:43-44,56 |
today và now lấy từ Clock theo múi JVM (D-08); hai lần đọc clock.instant(). |
SVC:48 / EW:60-66 |
NPE có chủ đích cho lô thiếu cấu hình cảnh báo (D-04) → cả lượt rollback. Đừng "sửa" thành bỏ qua dòng. |
SCH:47-55 |
Khoá ngoài transaction, không nhả; catch RuntimeException ngoài proxy (D-09, D-10); log chủ khoá khác cũ (F-logic-1). |